Replace TownSquare with HomeCamp art and land humanoid bots, LittleBear, and LmGirl review assets.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7,6 +7,7 @@ using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using UnityEngine.Rendering.Universal;
|
||||
using UnityEngine.SceneManagement;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
@@ -14,11 +15,16 @@ using Object = UnityEngine.Object;
|
||||
public static class BuildHomeCamp
|
||||
{
|
||||
const string Root = "Assets/Bundles/Scenes/Town/HomeCamp";
|
||||
const string Town = "Assets/Bundles/Scenes/Town/TownSquare";
|
||||
const string Shared = "Assets/Bundles/Scenes/Common";
|
||||
const string PrefabPath = Root + "/HomeCamp.prefab";
|
||||
const string ScenePath = Root + "/HomeCamp.unity";
|
||||
const int Layer = 29;
|
||||
const float GroundHalfSize = 21;
|
||||
const float StoneTileMeters = 3.2f;
|
||||
static readonly string[] Parts = { "ground", "mainhouse", "cottage", "fountain" };
|
||||
static readonly string[] Props = { "fence", "stall", "flowerpot", "rocks", "steps", "cypress", "bush", "flowercluster", "shadetree" };
|
||||
// Studio HomeCamp props were exported facing -Z. DGX static meshes use the project +Z front, so the prefab root stays unrotated.
|
||||
static readonly string[] LegacyMinusZ = { "fence", "stall", "flowerpot", "rocks", "steps", "cypress", "bush" };
|
||||
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; }
|
||||
@@ -29,6 +35,15 @@ public static class BuildHomeCamp
|
||||
{ var s = SceneManager.GetSceneAt(i); return s.path + " dirty=" + s.isDirty; }));
|
||||
}
|
||||
|
||||
public static string RenderInfo()
|
||||
{
|
||||
var a = GraphicsSettings.currentRenderPipeline as UniversalRenderPipelineAsset;
|
||||
if (!a) return "No URP asset";
|
||||
return QualitySettings.names[QualitySettings.GetQualityLevel()] + " asset=" + AssetDatabase.GetAssetPath(a)
|
||||
+ " mainShadows=" + a.supportsMainLightShadows + " distance=" + a.shadowDistance + " res=" + a.mainLightShadowmapResolution
|
||||
+ " cascades=" + a.shadowCascadeCount + " soft=" + a.supportsSoftShadows + " hdr=" + a.supportsHDR + " msaa=" + a.msaaSampleCount;
|
||||
}
|
||||
|
||||
static void Folder(string path)
|
||||
{
|
||||
if (AssetDatabase.IsValidFolder(path)) return;
|
||||
@@ -54,14 +69,15 @@ public static class BuildHomeCamp
|
||||
File.Copy(source, destination, true);
|
||||
AssetDatabase.ImportAsset(destination, ImportAssetOptions.ForceSynchronousImport);
|
||||
}
|
||||
static void TextureSettings(string path, bool srgb, bool backdrop = false)
|
||||
static void TextureSettings(string path, bool srgb, bool backdrop = false, bool tile = false, bool isProp = 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.mipmapEnabled = !backdrop; importer.maxTextureSize = isProp ? 512 : (tile ? 1024 : 2048);
|
||||
importer.textureCompression = (tile || isProp) ? TextureImporterCompression.Compressed : TextureImporterCompression.Uncompressed;
|
||||
importer.wrapMode = tile ? TextureWrapMode.Repeat : TextureWrapMode.Clamp;
|
||||
importer.anisoLevel = tile ? 4 : 1;
|
||||
importer.SaveAndReimport();
|
||||
}
|
||||
static Material SaveMaterial(string path, string shaderName)
|
||||
@@ -86,8 +102,53 @@ public static class BuildHomeCamp
|
||||
if (receipt.status != "ready_for_unity" || receipt.triangles * 10 > receipt.targetTriangles * 13)
|
||||
throw new InvalidOperationException("Unfinished model " + part);
|
||||
}
|
||||
foreach (var part in Parts)
|
||||
foreach (var part in Parts) ImportPart(part);
|
||||
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<Texture2D>(Root + "/backdrop.png"));
|
||||
background.SetColor("_BaseColor", Color.white);
|
||||
EditorUtility.SetDirty(background); AssetDatabase.SaveAssetIfDirty(background);
|
||||
BuildLookDev();
|
||||
return "Imported four Studio assets + XPbr, authored four component Prefabs, unlit backdrop and lit look-dev materials.";
|
||||
}
|
||||
|
||||
/// <summary>Imports the v2 B/C props whose five-step receipts are ready; returns the imported ids.</summary>
|
||||
public static string BuildProps()
|
||||
{
|
||||
if (EditorApplication.isPlaying) throw new InvalidOperationException("Stop Play before authoring.");
|
||||
AssetDatabase.ImportAsset("Assets/Bundles/Shaders/XShader/HomeCampXPbr.shader", ImportAssetOptions.ForceSynchronousImport);
|
||||
var done = new List<string>(); var skipped = new List<string>();
|
||||
foreach (var part in Props)
|
||||
{
|
||||
var path = Path.Combine(Repo, "Tools/output/homecamp_studio", part, "production.json");
|
||||
var receipt = File.Exists(path) ? JsonUtility.FromJson<Receipt>(File.ReadAllText(path)) : null;
|
||||
if (receipt == null || receipt.status != "ready_for_unity" || receipt.triangles * 10 > receipt.targetTriangles * 13)
|
||||
{ skipped.Add(part); continue; }
|
||||
ImportPart(part); done.Add(part);
|
||||
}
|
||||
return "Imported props: " + string.Join(",", done) + "; not ready: " + string.Join(",", skipped);
|
||||
}
|
||||
|
||||
/// <summary>Reimports cottage when its five-step receipt is ready (v2 awning side house).</summary>
|
||||
public static string BuildCottage()
|
||||
{
|
||||
if (EditorApplication.isPlaying) throw new InvalidOperationException("Stop Play before authoring.");
|
||||
AssetDatabase.ImportAsset("Assets/Bundles/Shaders/XShader/HomeCampXPbr.shader", ImportAssetOptions.ForceSynchronousImport);
|
||||
var path = Path.Combine(Repo, "Tools/output/homecamp_studio/cottage/production.json");
|
||||
var receipt = File.Exists(path) ? JsonUtility.FromJson<Receipt>(File.ReadAllText(path)) : null;
|
||||
if (receipt == null || receipt.status != "ready_for_unity" || receipt.triangles * 10 > receipt.targetTriangles * 13)
|
||||
throw new InvalidOperationException("Cottage not ready_for_unity");
|
||||
ImportPart("cottage");
|
||||
// Secondary UV for lightmaps (same as mainhouse/fountain).
|
||||
var importer = (ModelImporter)AssetImporter.GetAtPath(Root + "/cottage/cottage.fbx");
|
||||
if (!importer.generateSecondaryUV || importer.secondaryUVPackMargin < 8)
|
||||
{ importer.generateSecondaryUV = true; importer.secondaryUVPackMargin = 8; importer.SaveAndReimport(); }
|
||||
return "Imported cottage triangles=" + receipt.triangles;
|
||||
}
|
||||
|
||||
static void ImportPart(string part)
|
||||
{
|
||||
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" })
|
||||
@@ -99,12 +160,17 @@ public static class BuildHomeCamp
|
||||
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);
|
||||
bool isProp = Props.Contains(part);
|
||||
TextureSettings(dir + "/" + part + "_base.png", true, isProp: isProp);
|
||||
TextureSettings(dir + "/" + part + "_mix.png", false, isProp: isProp);
|
||||
var mat = SaveMaterial(dir + "/" + part + ".mat", "Test/HomeCampXPbr");
|
||||
mat.SetTexture("_MainTex", AssetDatabase.LoadAssetAtPath<Texture2D>(dir + "/" + part + "_base.png"));
|
||||
mat.SetTexture("_MixTex", AssetDatabase.LoadAssetAtPath<Texture2D>(dir + "/" + part + "_mix.png"));
|
||||
mat.SetColor("_BaseColor", Color.white); mat.SetFloat("_Metallic", 1); mat.SetFloat("_Roughness", 1); mat.SetFloat("_AO", 1);
|
||||
// Candy plaster: keep albedo readable, kill hot specular (fountain bake A~0.2 → smoothness 0.8 without scale).
|
||||
mat.SetColor("_BaseColor", new Color(1.08f, 1.06f, 1.03f, 1));
|
||||
mat.SetFloat("_Metallic", .05f); mat.SetFloat("_Roughness", 1); mat.SetFloat("_SmoothnessScale", .22f); mat.SetFloat("_AO", .65f);
|
||||
mat.SetFloat("_LightmapShadeGamma", .58f);
|
||||
mat.enableInstancing = true;
|
||||
EditorUtility.SetDirty(mat); AssetDatabase.SaveAssetIfDirty(mat);
|
||||
var scene = EditorSceneManager.NewPreviewScene();
|
||||
try
|
||||
@@ -116,59 +182,174 @@ public static class BuildHomeCamp
|
||||
if (part == "ground")
|
||||
foreach (var f in go.GetComponentsInChildren<MeshFilter>())
|
||||
{ var c = f.gameObject.AddComponent<MeshCollider>(); c.sharedMesh = f.sharedMesh; c.convex = false; }
|
||||
PrefabUtility.SaveAsPrefabAsset(go, dir + "/" + part + ".prefab");
|
||||
var root = go;
|
||||
if (Props.Contains(part))
|
||||
{
|
||||
root = new GameObject(part); SceneManager.MoveGameObjectToScene(root, scene);
|
||||
go.transform.SetParent(root.transform, false); go.name = part + "_mesh";
|
||||
if (LegacyMinusZ.Contains(part))
|
||||
go.transform.localRotation = Quaternion.Euler(0, 180, 0);
|
||||
}
|
||||
PrefabUtility.SaveAsPrefabAsset(root, dir + "/" + part + ".prefab");
|
||||
}
|
||||
finally { EditorSceneManager.ClosePreviewScene(scene); }
|
||||
}
|
||||
|
||||
/// <summary>Lit foliage cards, tiled XTerrain ground and post Volume; does not reimport Studio models.</summary>
|
||||
public static string BuildLookDev()
|
||||
{
|
||||
if (EditorApplication.isPlaying) throw new InvalidOperationException("Stop Play before authoring.");
|
||||
AssetDatabase.ImportAsset("Assets/Bundles/Shaders/XShader/HomeCampXPbr.shader", ImportAssetOptions.ForceSynchronousImport);
|
||||
AssetDatabase.ImportAsset("Assets/Bundles/Shaders/XShader/XTerrain.shader", ImportAssetOptions.ForceSynchronousImport);
|
||||
BuildCardMaterials();
|
||||
BuildGroundSurface();
|
||||
BuildVolumeProfile();
|
||||
BuildLightingSettings();
|
||||
TuneXPbrMaterials();
|
||||
// Lightmap UVs go to UV1 only; Studio UV0 and baked XPbr textures stay untouched.
|
||||
foreach (var part in new[] { "mainhouse", "fountain", "cottage" })
|
||||
{
|
||||
var importer = (ModelImporter)AssetImporter.GetAtPath(Root + "/" + part + "/" + part + ".fbx");
|
||||
if (!importer) continue;
|
||||
if (importer.generateSecondaryUV && importer.secondaryUVPackMargin >= 8) continue;
|
||||
importer.generateSecondaryUV = true; importer.secondaryUVPackMargin = 8; importer.SaveAndReimport();
|
||||
}
|
||||
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<Texture2D>(Root + "/backdrop.png"));
|
||||
background.SetColor("_BaseColor", Color.white);
|
||||
EditorUtility.SetDirty(background); AssetDatabase.SaveAssetIfDirty(background);
|
||||
return "Authored lit foliage cards, XTerrain ground, HomeCampVolume, lighting settings and lightmap UVs.";
|
||||
}
|
||||
|
||||
/// <summary>Matte candy response: lower metallic/smoothness, slight albedo lift for readable diffuse.</summary>
|
||||
static void TuneXPbrMaterials()
|
||||
{
|
||||
foreach (var path in AssetDatabase.FindAssets("t:Material", new[] { Root })
|
||||
.Select(AssetDatabase.GUIDToAssetPath)
|
||||
.Where(p => p.EndsWith(".mat")))
|
||||
{
|
||||
var mat = AssetDatabase.LoadAssetAtPath<Material>(path);
|
||||
if (!mat || mat.shader == null || mat.shader.name != "Test/HomeCampXPbr") continue;
|
||||
mat.SetColor("_BaseColor", new Color(1.08f, 1.06f, 1.03f, 1));
|
||||
mat.SetFloat("_Metallic", .05f);
|
||||
mat.SetFloat("_Roughness", 1);
|
||||
mat.SetFloat("_SmoothnessScale", .22f);
|
||||
mat.SetFloat("_AO", .65f);
|
||||
mat.SetFloat("_LightmapShadeGamma", .58f);
|
||||
mat.enableInstancing = true;
|
||||
EditorUtility.SetDirty(mat); AssetDatabase.SaveAssetIfDirty(mat);
|
||||
}
|
||||
}
|
||||
|
||||
static LightingSettings BuildLightingSettings()
|
||||
{
|
||||
var path = Root + "/HomeCampLighting.lighting";
|
||||
var settings = AssetDatabase.LoadAssetAtPath<LightingSettings>(path);
|
||||
if (!settings) { settings = new LightingSettings { name = "HomeCampLighting" }; AssetDatabase.CreateAsset(settings, path); }
|
||||
settings.bakedGI = true; settings.realtimeGI = false;
|
||||
settings.lightmapper = LightingSettings.Lightmapper.ProgressiveGPU;
|
||||
// Fully-baked sun stores cast shadows in lightmap RGB (offscreen camera.Render has no realtime shadowmaps).
|
||||
settings.mixedBakeMode = MixedLightingMode.Subtractive;
|
||||
settings.lightmapResolution = 16; settings.lightmapPadding = 4; settings.lightmapMaxSize = 1024;
|
||||
settings.directionalityMode = LightmapsMode.NonDirectional;
|
||||
// Enough bounces for readable shade walls; keep indirect modest so plaza cast shadows stay dark.
|
||||
settings.directSampleCount = 48; settings.indirectSampleCount = 512; settings.environmentSampleCount = 128; settings.maxBounces = 3;
|
||||
settings.ao = true; settings.aoMaxDistance = .75f; settings.aoExponentDirect = .55f; settings.aoExponentIndirect = .7f;
|
||||
settings.albedoBoost = 1f; settings.indirectScale = 1f;
|
||||
settings.lightmapCompression = LightmapCompression.NormalQuality; settings.filteringMode = LightingSettings.FilterMode.Auto;
|
||||
EditorUtility.SetDirty(settings); AssetDatabase.SaveAssetIfDirty(settings);
|
||||
return settings;
|
||||
}
|
||||
|
||||
static void BuildCardMaterials()
|
||||
{
|
||||
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<Texture2D>(Root + "/" + name + ".png"));
|
||||
var card = SaveMaterial(Root + "/" + name + ".mat", "Universal Render Pipeline/Simple Lit");
|
||||
var texture = AssetDatabase.LoadAssetAtPath<Texture2D>(Root + "/" + name + ".png");
|
||||
// The lightmapper reads cutout transparency from _MainTex/_Cutoff.
|
||||
card.SetTexture("_BaseMap", texture); card.SetTexture("_MainTex", texture);
|
||||
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.SetFloat("_ReceiveShadows", 1); card.DisableKeyword("_RECEIVE_SHADOWS_OFF");
|
||||
card.SetFloat("_SpecularHighlights", 0); card.DisableKeyword("_SPECULAR_COLOR");
|
||||
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 Texture2D GroundLayer(string name, bool srgb, bool tile)
|
||||
{
|
||||
Source(Path.Combine(Repo, "Doc/Design/Scene/HomeCamp", name + ".png"), Root + "/" + name + ".png");
|
||||
TextureSettings(Root + "/" + name + ".png", srgb, false, tile);
|
||||
return AssetDatabase.LoadAssetAtPath<Texture2D>(Root + "/" + name + ".png");
|
||||
}
|
||||
|
||||
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<Texture2D>(Root + "/ground_surface.png"));
|
||||
mat.SetColor("_BaseColor", Color.white);
|
||||
const float half = GroundHalfSize;
|
||||
var stone = GroundLayer("ground_tile_stone", true, true);
|
||||
var grass = GroundLayer("ground_tile_grass", true, true);
|
||||
var grassDark = GroundLayer("ground_tile_grass_dark", true, true);
|
||||
var control = GroundLayer("ground_control", false, false);
|
||||
var mat = SaveMaterial(Root + "/ground_terrain.mat", "XWorld/XTerrain");
|
||||
mat.SetTexture("_LayerTex0", stone); mat.SetTexture("_LayerTex1", grass);
|
||||
mat.SetTexture("_LayerTex2", grassDark); mat.SetTexture("_LayerTex3", grassDark);
|
||||
mat.SetTexture("_Control", control);
|
||||
mat.SetFloat("_Tiling", half * 2 / StoneTileMeters);
|
||||
// Softer micro-relief so the lawn reads as soft grass rather than shiny plastic.
|
||||
// High _Roughness bias → nearly matte ground; metallic off so stone/grass stay diffuse-led.
|
||||
mat.SetFloat("_BumpScale", 1.6f); mat.SetFloat("_Roughness", 1.15f); mat.SetFloat("_Metallic", 0); mat.SetFloat("_AO", .9f);
|
||||
// Less GI lift → ~20% deeper shade on busy ground albedo.
|
||||
mat.SetFloat("_LightmapContrast", .58f);
|
||||
mat.SetFloat("_LightmapShadeMul", 1f);
|
||||
mat.SetColor("_LightmapShadeTint", new Color(.88f, .9f, .95f, 1f));
|
||||
EditorUtility.SetDirty(mat); AssetDatabase.SaveAssetIfDirty(mat);
|
||||
var mesh = AssetDatabase.LoadAssetAtPath<Mesh>(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.vertices = new[] {new Vector3(-half,0,-half), new Vector3(-half,0,half), new Vector3(half,0,half), new Vector3(half,0,-half)};
|
||||
mesh.uv = new[] {new Vector2(0,0),new Vector2(0,1),new Vector2(1,1),new Vector2(1,0)};
|
||||
mesh.uv2 = mesh.uv;
|
||||
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<MeshFilter>().sharedMesh = mesh; go.AddComponent<MeshRenderer>().sharedMaterial = mat;
|
||||
go.AddComponent<MeshFilter>().sharedMesh = mesh;
|
||||
var renderer = go.AddComponent<MeshRenderer>(); renderer.sharedMaterial = mat;
|
||||
renderer.shadowCastingMode = ShadowCastingMode.Off; renderer.receiveShadows = true;
|
||||
go.AddComponent<MeshCollider>().sharedMesh = mesh;
|
||||
PrefabUtility.SaveAsPrefabAsset(go, Root + "/GroundSurface.prefab");
|
||||
}
|
||||
finally { EditorSceneManager.ClosePreviewScene(scene); }
|
||||
}
|
||||
|
||||
static VolumeProfile BuildVolumeProfile()
|
||||
{
|
||||
var path = Root + "/HomeCampVolume.asset";
|
||||
var profile = AssetDatabase.LoadAssetAtPath<VolumeProfile>(path);
|
||||
if (!profile) { profile = ScriptableObject.CreateInstance<VolumeProfile>(); AssetDatabase.CreateAsset(profile, path); }
|
||||
foreach (var old in profile.components.ToArray()) { profile.components.Remove(old); Object.DestroyImmediate(old, true); }
|
||||
T Add<T>() where T : VolumeComponent
|
||||
{
|
||||
var c = profile.Add<T>(true); c.name = typeof(T).Name;
|
||||
c.hideFlags = HideFlags.HideInInspector | HideFlags.HideInHierarchy;
|
||||
AssetDatabase.AddObjectToAsset(c, profile); return c;
|
||||
}
|
||||
Add<Tonemapping>().mode.value = TonemappingMode.Neutral;
|
||||
var color = Add<ColorAdjustments>();
|
||||
// Noon frame + another ~20% brightness (EV += log2(1.2)); mild contrast keeps shade soft-PBR.
|
||||
color.postExposure.value = .74f; color.contrast.value = 10; color.saturation.value = 5; color.hueShift.value = 0;
|
||||
color.colorFilter.value = Color.white;
|
||||
var white = Add<WhiteBalance>(); white.temperature.value = 6; white.tint.value = 0;
|
||||
var bloom = Add<Bloom>(); bloom.threshold.value = 1.15f; bloom.intensity.value = .12f; bloom.scatter.value = .55f;
|
||||
var vignette = Add<Vignette>(); vignette.intensity.value = .18f; vignette.smoothness.value = .45f;
|
||||
EditorUtility.SetDirty(profile); AssetDatabase.SaveAssetIfDirty(profile);
|
||||
return profile;
|
||||
}
|
||||
|
||||
static GameObject Place(Transform parent, string path, string name, Vector3 pos, float size, bool width = false, bool faceCenter = false)
|
||||
{
|
||||
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
||||
@@ -193,7 +374,7 @@ public static class BuildHomeCamp
|
||||
var name = part == "bush" ? "shrub" : "tree";
|
||||
return "Assets/Bundles/Scenes/Prototype/Battlefield/A/Components/" + name + "/" + name + ".prefab";
|
||||
}
|
||||
return Town + "/" + part + "/" + part + ".prefab";
|
||||
return Shared + "/" + part + "/" + part + ".prefab";
|
||||
}
|
||||
static float GroundY(GameObject ground, float x, float z)
|
||||
{
|
||||
@@ -209,34 +390,78 @@ public static class BuildHomeCamp
|
||||
var go = GameObject.CreatePrimitive(PrimitiveType.Quad); go.name = name;
|
||||
go.transform.SetParent(parent, false); Object.DestroyImmediate(go.GetComponent<Collider>());
|
||||
var texture = AssetDatabase.LoadAssetAtPath<Texture2D>(Root + "/" + image + ".png");
|
||||
var rotation = Quaternion.LookRotation(new Vector3(0, 1.8f, 3) - new Vector3(0, 15, -27), Vector3.up);
|
||||
var rotation = Quaternion.LookRotation(CamTarget - CamPos, 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>(); renderer.sharedMaterial = AssetDatabase.LoadAssetAtPath<Material>(Root + "/" + image + ".mat");
|
||||
renderer.shadowCastingMode = ShadowCastingMode.Off; renderer.receiveShadows = false;
|
||||
// Trees cast into the lightmap again (concept art needs canopy shadows); kept behind roofs to avoid plaza ink.
|
||||
renderer.shadowCastingMode = ShadowCastingMode.On;
|
||||
renderer.receiveShadows = true;
|
||||
SetLayer(go); return go;
|
||||
}
|
||||
static readonly Vector3 CamPos = new Vector3(0, 14, -25), CamTarget = new Vector3(0, 1.6f, 3);
|
||||
const float CamFov = 50;
|
||||
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<Camera>();
|
||||
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;
|
||||
go.transform.position = CamPos;
|
||||
go.transform.LookAt(CamTarget);
|
||||
camera.fieldOfView = CamFov; 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;
|
||||
camera.allowHDR = true; camera.allowMSAA = true;
|
||||
var data = camera.GetUniversalAdditionalCameraData();
|
||||
data.renderPostProcessing = true; data.volumeLayerMask = 1 << Layer;
|
||||
return camera;
|
||||
}
|
||||
static void Light(Scene scene, Transform parent)
|
||||
static Light 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>(); 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);
|
||||
// Key: warm sun. Fill comes from a separate baked skylight so Subtractive maps do not crush backfaces.
|
||||
// Fully Baked sun: cast shadows live in the lightmap RGB. Mixed/Subtractive still needs
|
||||
// realtime shadow maps, which offscreen camera.Render does not produce — so review looked shadowless.
|
||||
// Noon key: high elevation, +30% intensity vs prior look-dev; shade deepened in bake/shaders.
|
||||
light.color = new Color(1, .98f, .94f); light.intensity = 2.15f; light.shadows = LightShadows.Soft;
|
||||
light.lightmapBakeType = LightmapBakeType.Baked; light.shadowAngle = 4;
|
||||
light.shadowStrength = .94f; light.shadowBias = .05f; light.shadowNormalBias = .3f;
|
||||
light.cullingMask = 1 << Layer; go.transform.rotation = Quaternion.Euler(68, 50, 0);
|
||||
BakeFillLight(scene, parent);
|
||||
return light;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Baked-only opposite skylight (no shadows). URP ignores a second realtime Directional,
|
||||
/// but Progressive still bakes this into lightmaps so backfaces stay cream while cast shadows remain.
|
||||
/// </summary>
|
||||
static void BakeFillLight(Scene scene, Transform parent)
|
||||
{
|
||||
var go = new GameObject("HomeCampBakeFill"); SceneManager.MoveGameObjectToScene(go, scene);
|
||||
if (parent) go.transform.SetParent(parent, false);
|
||||
var light = go.AddComponent<Light>(); light.type = LightType.Directional;
|
||||
// Noon skylight fill — held back another ~20% so cast shade reads deeper.
|
||||
light.color = new Color(.82f, .88f, 1f); light.intensity = .22f;
|
||||
light.shadows = LightShadows.None; light.lightmapBakeType = LightmapBakeType.Baked;
|
||||
light.cullingMask = 1 << Layer;
|
||||
go.transform.rotation = Quaternion.Euler(75, -30, 0);
|
||||
}
|
||||
|
||||
/// <summary>Grid of probes over the plaza so houses on LightProbes get baked sky fill, not black tetrahedra.</summary>
|
||||
static void PlaceLightProbes(Transform parent)
|
||||
{
|
||||
var go = new GameObject("HomeCampLightProbes");
|
||||
go.transform.SetParent(parent, false);
|
||||
var group = go.AddComponent<LightProbeGroup>();
|
||||
var positions = new System.Collections.Generic.List<Vector3>();
|
||||
for (int x = -10; x <= 10; x += 4)
|
||||
for (int z = -12; z <= 12; z += 4)
|
||||
for (int y = 1; y <= 5; y += 2)
|
||||
positions.Add(new Vector3(x, y, z));
|
||||
group.probePositions = positions.ToArray();
|
||||
}
|
||||
static void Background(Transform parent, Camera camera)
|
||||
{
|
||||
@@ -260,6 +485,13 @@ public static class BuildHomeCamp
|
||||
if (EditorApplication.isPlaying) throw new InvalidOperationException("Author in Edit Mode.");
|
||||
var previous = SceneManager.GetActiveScene();
|
||||
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Additive);
|
||||
// An already-open HomeCamp keeps this path. Saving the new scene, then baking the stale one, writes the cards back over the file.
|
||||
var stale = SceneManager.GetSceneByPath(ScenePath);
|
||||
if (stale.IsValid() && stale.isLoaded && stale != scene)
|
||||
{
|
||||
if (previous == stale) previous = default;
|
||||
EditorSceneManager.CloseScene(stale, true);
|
||||
}
|
||||
try
|
||||
{
|
||||
SceneManager.SetActiveScene(scene);
|
||||
@@ -287,38 +519,153 @@ public static class BuildHomeCamp
|
||||
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);
|
||||
// Left side uses the awning cottage to match the reference market house; right stays a mirrored mainhouse.
|
||||
Put("cottage", "HouseLeft", -6.8f, 3.6f, 3.8f, false, true);
|
||||
Put("mainhouse", "HouseRight", 7.5f, 4, 4, false, true);
|
||||
// Mirrored copy so the reused house does not read as an identical clone; HomeCampXPbr is two-sided.
|
||||
var houseRight = root.transform.Find("HouseRight");
|
||||
houseRight.localScale = Vector3.Scale(houseRight.localScale, new Vector3(-1, 1, 1));
|
||||
// Sink house bases a few centimeters to hide the mesh/ground seam seen in portrait review.
|
||||
foreach (var house in new[] { "MainHouse", "HouseLeft", "HouseRight" })
|
||||
root.transform.Find(house).position += Vector3.down * .08f;
|
||||
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);
|
||||
// Keep canopy trees behind the roofs so they frame the backdrop without dunking HouseLeft in shadow.
|
||||
// Nearer canopy slots are placed after the viewer vector, as DGX meshes when ready.
|
||||
// TreeRearLeft / TreeRearRight stay cards: they sit behind the roofs and another 2000 tris each is not worth it.
|
||||
Put("tree", "TreeRearLeft", -5.6f, 14.5f, 4.6f, false, false, true);
|
||||
Put("tree", "TreeRearRight", 5.6f, 14.5f, 4.6f, 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("lamp", "LampPath", 3.4f, -8.4f, 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);
|
||||
}
|
||||
Put("bench", "BenchRight", 6.8f, -2.2f, 2.2f, true, true, true);
|
||||
// Fountain ring is placed after the viewer vector: six flowercluster meshes, or the old cards if the prefab is missing.
|
||||
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);
|
||||
// Flower clusters along the plaza rim and the path so the stone/grass edge is never a bare cut.
|
||||
var rim = 0;
|
||||
for (int i = 0; i < 18; i++)
|
||||
{
|
||||
var angle = (-80 + i * 20f) * Mathf.Deg2Rad;
|
||||
var x = Mathf.Sin(angle) * 9.3f; var z = 3.5f + Mathf.Cos(angle) * 6.9f;
|
||||
if (z < -2 && Mathf.Abs(x) < 2.5f) continue;
|
||||
Put("bush", "RimFlowers" + rim++, x, z, .7f + (i % 3) * .12f, false, false, true);
|
||||
}
|
||||
var pathFlowers = new[] {
|
||||
new Vector2(-.6f,-7.5f), new Vector2(.3f,-12.5f),
|
||||
new Vector2(1.3f,-16), new Vector2(4.6f,-15.5f), new Vector2(-3.2f,-14.2f), new Vector2(3.2f,-14.8f),
|
||||
new Vector2(5.1f,-10.2f), new Vector2(.8f,-18.5f) };
|
||||
for (int i = 0; i < pathFlowers.Length; i++)
|
||||
Put("bush", "PathFlowers" + i, pathFlowers[i].x, pathFlowers[i].y, .65f + (i % 3) * .08f, false, false, true);
|
||||
void Prop(string part, string name, float x, float z, float size, bool width, Vector3 face, bool presentToCamera = false)
|
||||
{
|
||||
if (!AssetDatabase.LoadAssetAtPath<GameObject>(PartPath(part))) { placements.Add(name + " skipped (no prefab)"); return; }
|
||||
var y = GroundY(ground, x, z);
|
||||
var go = Place(root.transform, PartPath(part), name, new Vector3(x, y, z), size, width);
|
||||
if (presentToCamera)
|
||||
go.transform.rotation = Quaternion.LookRotation(CamTarget - CamPos, Vector3.up);
|
||||
else
|
||||
{
|
||||
var toward = face - new Vector3(x, face.y, z);
|
||||
go.transform.rotation = Quaternion.LookRotation(toward, Vector3.up);
|
||||
}
|
||||
go.transform.position += Vector3.up * (y - BoundsOf(go).min.y);
|
||||
placements.Add(name + " groundY=" + y.ToString("F3"));
|
||||
}
|
||||
var plaza = new Vector3(0, 0, 2); var viewer = new Vector3(CamPos.x, 0, CamPos.z);
|
||||
// Portrait horizontal view is about 29 degrees. Card slots at |x|>=11 fall outside it,
|
||||
// so the shade trees stand in the lawn beside the roofs instead of clipping the frame edge.
|
||||
if (AssetDatabase.LoadAssetAtPath<GameObject>(PartPath("shadetree")))
|
||||
{
|
||||
Prop("shadetree", "TreeBackLeft", -5.6f, 10.4f, 6.2f, false, viewer);
|
||||
Prop("shadetree", "TreeBackRight", 6.2f, 9.6f, 5.7f, false, plaza);
|
||||
}
|
||||
else
|
||||
{
|
||||
Put("tree", "TreeBackLeft", -5.6f, 10.4f, 6.2f, false, false, true);
|
||||
Put("tree", "TreeBackRight", 6.2f, 9.6f, 5.7f, false, false, true);
|
||||
}
|
||||
foreach (var (n, x, z, s) in new[] {
|
||||
("ClusterNearL", -2.4f, -11.2f, .62f), ("ClusterNearR", 3.6f, -12.2f, .7f),
|
||||
("ClusterPathL", -4.6f, -8.6f, .55f), ("ClusterPlaza", 2.6f, -6.4f, .5f) })
|
||||
Prop("flowercluster", n, x, z, s, false, viewer);
|
||||
if (AssetDatabase.LoadAssetAtPath<GameObject>(PartPath("flowercluster")))
|
||||
{
|
||||
// Blossoms are on local +Z. Yaw-only leaves the leafy top toward this high camera, so present the front.
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
var angle = (i * 60f) * Mathf.Deg2Rad;
|
||||
var size = .62f + (i % 3) * .06f;
|
||||
Prop("flowercluster", "FountainCluster" + i, Mathf.Sin(angle) * 3.25f, .8f + Mathf.Cos(angle) * 3.25f, size, false, viewer, true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
// StallLeft removed: left cottage already carries the yellow awning counter.
|
||||
Prop("stall", "StallRight", 4.5f, 2.8f, 2.4f, false, plaza);
|
||||
Prop("steps", "StepsRear", 4.3f, 10.2f, 2.6f, true, viewer);
|
||||
Prop("cypress", "CypressLeft", -4.3f, 9.8f, 5, false, viewer);
|
||||
Prop("cypress", "CypressRight", 7.8f, 8.6f, 5.6f, false, viewer);
|
||||
Prop("cypress", "CypressFrontLeft", -6.8f, -3.4f, 4.6f, false, viewer);
|
||||
// Portrait frustum is only about +-5.4m wide at z=-10, so foreground props stay inside it.
|
||||
foreach (var (n, x, z) in new[] { ("FenceLeftA", -5.2f, -8.6f), ("FenceLeftB", -3.4f, -11), ("FenceRightA", 5.4f, -9.6f) })
|
||||
Prop("fence", n, x, z, 2.8f, true, plaza);
|
||||
// Single-pot prefab scaled into small clusters (three-pot Studio mesh failed UV-preserving reduction).
|
||||
foreach (var (n, x, z, s) in new[] {
|
||||
("PotMainL1", -2.9f, 4.8f, .7f), ("PotMainL2", -2.4f, 5.1f, .55f), ("PotMainL3", -2.55f, 4.45f, .48f),
|
||||
("PotMainR1", 2.9f, 4.9f, .7f), ("PotMainR2", 3.35f, 5.15f, .55f), ("PotMainR3", 3.1f, 4.5f, .48f),
|
||||
("PotLeft1", -6.5f, 1.4f, .65f), ("PotLeft2", -6.05f, 1.7f, .5f),
|
||||
("PotRight1", 6.7f, 1.8f, .65f), ("PotRight2", 7.1f, 1.5f, .5f) })
|
||||
Prop("flowerpot", n, x, z, s, false, viewer);
|
||||
foreach (var (n, x, z, s) in new[] {
|
||||
("RockFrontLeft", -1.8f, -13.6f, 1.6f), ("RockFrontRight", 4.6f, -13.4f, 1.3f), ("RockLeft", -6, -6.4f, 1.2f),
|
||||
("RockNear", -.4f, -15.8f, 1.1f), ("RockPath", 2.8f, -11.2f, .95f) })
|
||||
Prop("rocks", n, x, z, s, true, viewer);
|
||||
foreach (var (n, x, z, s) in new[] {
|
||||
("BushFrontLeft", -4.6f, -12.8f, 1.5f), ("BushFront", -1.4f, -9.6f, 1.1f), ("BushFrontRight", 5.6f, -12.2f, 1.3f),
|
||||
("BushRight", 6.4f, -6.2f, 1.3f), ("BushLeft", -7.2f, -1.6f, 1.4f), ("BushRearLeft", -5.6f, 7.8f, 1.2f),
|
||||
("BushRearRight", 6, 8, 1.2f), ("BushNearLeft", -3.6f, -15.2f, 1.25f), ("BushNearRight", 3.8f, -15.6f, 1.15f),
|
||||
("BushMidLeft", -5.4f, -7.4f, 1.2f) })
|
||||
Prop("bush", n, x, z, s, true, viewer);
|
||||
PlaceLightProbes(root.transform);
|
||||
var camera = MakeCamera(scene, root.transform);
|
||||
Light(scene, root.transform); Background(root.transform, camera); SetLayer(root);
|
||||
var sun = Light(scene, root.transform); Background(root.transform, camera);
|
||||
var volume = new GameObject("PostVolume").AddComponent<Volume>();
|
||||
volume.transform.SetParent(root.transform, false);
|
||||
volume.isGlobal = true; volume.priority = 10;
|
||||
volume.sharedProfile = AssetDatabase.LoadAssetAtPath<VolumeProfile>(Root + "/HomeCampVolume.asset");
|
||||
if (!volume.sharedProfile) throw new InvalidOperationException("Run BuildLookDev before Build.");
|
||||
SetLayer(root);
|
||||
RenderSettings.sun = sun;
|
||||
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);
|
||||
// Noon ambient ~+30% vs prior; still cooler in shade so ground cast shadows stay visible.
|
||||
RenderSettings.ambientIntensity = .9f;
|
||||
RenderSettings.ambientSkyColor = new Color(.85f, .9f, .98f);
|
||||
RenderSettings.ambientEquatorColor = new Color(.72f, .7f, .64f);
|
||||
RenderSettings.ambientGroundColor = new Color(.55f, .52f, .46f);
|
||||
RenderSettings.fog = true; RenderSettings.fogMode = FogMode.Linear;
|
||||
RenderSettings.fogColor = new Color(.78f, .88f, .95f);
|
||||
RenderSettings.fogStartDistance = 45; RenderSettings.fogEndDistance = 220;
|
||||
MarkStatic(root);
|
||||
Lightmapping.lightingSettings = AssetDatabase.LoadAssetAtPath<LightingSettings>(Root + "/HomeCampLighting.lighting");
|
||||
if (!Lightmapping.lightingSettings) throw new InvalidOperationException("Run BuildLookDev before Build.");
|
||||
// Subtractive fills shadowed texels with this color; keep it close to sunlit grass so maps stay soft.
|
||||
// Darker than lit plaster so cast shadows read, brighter than ink so walls stay detailed.
|
||||
RenderSettings.subtractiveShadowColor = new Color(.38f, .44f, .55f);
|
||||
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.";
|
||||
// Review-only 1.6m stand-in; kept in the scene outside the Prefab, disabled for art captures.
|
||||
ScaleReference(ground).SetActive(false);
|
||||
EditorSceneManager.SaveScene(scene, ScenePath);
|
||||
return "Saved " + PrefabPath + " and " + ScenePath + "; " + placements.Count + " placements. Run Bake next.";
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -326,6 +673,101 @@ public static class BuildHomeCamp
|
||||
EditorSceneManager.CloseScene(scene, true);
|
||||
}
|
||||
}
|
||||
// Ground + buildings lightmapped so Subtractive bake stores visible cast shadows for review captures.
|
||||
static readonly string[] Lightmapped = { "Ground", "MainHouse", "HouseLeft", "HouseRight", "Fountain" };
|
||||
static void MarkStatic(GameObject root)
|
||||
{
|
||||
foreach (var renderer in root.GetComponentsInChildren<Renderer>(true))
|
||||
{
|
||||
var top = renderer.transform;
|
||||
while (top.parent && top.parent != root.transform) top = top.parent;
|
||||
var name = top.name;
|
||||
if (name == "DistantLakeWaterfallCastle" || name.Contains("Flowers")) continue;
|
||||
GameObjectUtility.SetStaticEditorFlags(renderer.gameObject, StaticEditorFlags.ContributeGI | StaticEditorFlags.BatchingStatic);
|
||||
// Lamps, benches and tree cards cast into the lightmap but are lit from probes, so shared assets need no lightmap UVs.
|
||||
if (renderer is MeshRenderer mesh) mesh.receiveGI = Lightmapped.Contains(name) ? ReceiveGI.Lightmaps : ReceiveGI.LightProbes;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Bakes HomeCamp.unity alone, renders review captures, then restores the previously open scenes.</summary>
|
||||
public static async System.Threading.Tasks.Task<string> Bake()
|
||||
{
|
||||
if (EditorApplication.isPlaying) throw new InvalidOperationException("Stop Play before baking.");
|
||||
if (Enumerable.Range(0, SceneManager.sceneCount).Any(i => SceneManager.GetSceneAt(i).isDirty))
|
||||
throw new InvalidOperationException("An open scene has unsaved edits; save or discard it before baking.");
|
||||
var setup = EditorSceneManager.GetSceneManagerSetup();
|
||||
try
|
||||
{
|
||||
// Bake with HomeCamp as the only open scene so other scenes get no lighting data.
|
||||
// Drop a stale in-memory copy first; OpenScene will not reload a scene that is already loaded.
|
||||
var loaded = SceneManager.GetSceneByPath(ScenePath);
|
||||
if (loaded.IsValid() && loaded.isLoaded)
|
||||
{
|
||||
var holder = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Additive);
|
||||
EditorSceneManager.CloseScene(loaded, true);
|
||||
EditorSceneManager.CloseScene(holder, true);
|
||||
}
|
||||
EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Single);
|
||||
if (!Lightmapping.BakeAsync()) throw new InvalidOperationException("Lightmap bake could not start.");
|
||||
var started = DateTime.UtcNow;
|
||||
while (Lightmapping.isRunning)
|
||||
{
|
||||
if ((DateTime.UtcNow - started).TotalMinutes > 25) { Lightmapping.Cancel(); throw new TimeoutException("Lightmap bake exceeded 25 minutes."); }
|
||||
await System.Threading.Tasks.Task.Delay(500);
|
||||
}
|
||||
// Lightmaps are assigned to the scene a few editor ticks after isRunning clears.
|
||||
for (int i = 0; i < 40 && LightmapSettings.lightmaps.Length == 0; i++) await System.Threading.Tasks.Task.Delay(250);
|
||||
if (LightmapSettings.lightmaps.Length == 0) throw new InvalidOperationException("Bake produced no lightmaps.");
|
||||
EditorSceneManager.SaveScene(SceneManager.GetSceneByPath(ScenePath));
|
||||
return Capture() + " Bake " + (DateTime.UtcNow - started).TotalSeconds.ToString("F0") + "s.";
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (setup.Length > 0 && !setup.Any(s => s.path == ScenePath && s.isActive && setup.Length == 1))
|
||||
EditorSceneManager.RestoreSceneManagerSetup(setup);
|
||||
}
|
||||
}
|
||||
|
||||
public static string Capture()
|
||||
{
|
||||
var previous = SceneManager.GetActiveScene();
|
||||
var scene = SceneManager.GetSceneByPath(ScenePath);
|
||||
var opened = !scene.IsValid() || !scene.isLoaded;
|
||||
if (opened) scene = EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Additive);
|
||||
try
|
||||
{
|
||||
SceneManager.SetActiveScene(scene);
|
||||
var root = scene.GetRootGameObjects().Single(g => g.name == "HomeCamp");
|
||||
var camera = root.GetComponentInChildren<Camera>();
|
||||
var scale = scene.GetRootGameObjects().Single(g => g.name.StartsWith("ReviewOnly_ScaleReference"));
|
||||
scale.SetActive(true);
|
||||
CaptureCamera(camera, 1080, 1920, "homecamp_scale_reference.png");
|
||||
scale.SetActive(false);
|
||||
CaptureCamera(camera, 1080, 1920, "homecamp_portrait.png");
|
||||
CaptureCamera(camera, 1080, 2400, "homecamp_tall.png");
|
||||
var maps = LightmapSettings.lightmaps.Length;
|
||||
var ground = root.transform.Find("Ground")?.GetComponentInChildren<MeshRenderer>();
|
||||
var gi = ground ? (" groundLM=" + ground.lightmapIndex + " scaleOffset=" + ground.lightmapScaleOffset) : "";
|
||||
return "Rendered scale/portrait/tall with " + maps + " lightmap(s)." + gi;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (previous.IsValid() && previous.isLoaded) SceneManager.SetActiveScene(previous);
|
||||
if (opened) EditorSceneManager.CloseScene(scene, true);
|
||||
}
|
||||
}
|
||||
|
||||
static GameObject ScaleReference(GameObject ground)
|
||||
{
|
||||
var mat = SaveMaterial(Root + "/review_scale.mat", "Universal Render Pipeline/Lit");
|
||||
mat.SetColor("_BaseColor", new Color(.55f, .55f, .58f)); EditorUtility.SetDirty(mat); AssetDatabase.SaveAssetIfDirty(mat);
|
||||
var go = GameObject.CreatePrimitive(PrimitiveType.Capsule); go.name = "ReviewOnly_ScaleReference_1.6m";
|
||||
Object.DestroyImmediate(go.GetComponent<Collider>());
|
||||
go.transform.localScale = new Vector3(.45f, .8f, .45f);
|
||||
go.transform.position = new Vector3(2.8f, GroundY(ground, 2.8f, -2.5f) + .8f, -2.5f);
|
||||
go.GetComponent<Renderer>().sharedMaterial = mat;
|
||||
SetLayer(go); return go;
|
||||
}
|
||||
static void CaptureCamera(Camera camera, int width, int height, string name)
|
||||
{
|
||||
Directory.CreateDirectory(Review);
|
||||
@@ -344,6 +786,7 @@ public static class BuildHomeCamp
|
||||
}
|
||||
public static string CaptureParts() => CaptureSet(Parts);
|
||||
public static string CaptureReuse() => CaptureSet(new[] { "tree", "lamp", "bench", "bush" });
|
||||
public static string CaptureProps() => CaptureSet(Props.Where(p => AssetDatabase.LoadAssetAtPath<GameObject>(PartPath(p))));
|
||||
static string CaptureSet(IEnumerable<string> parts)
|
||||
{
|
||||
foreach (var part in parts)
|
||||
@@ -351,7 +794,7 @@ public static class BuildHomeCamp
|
||||
var scene = EditorSceneManager.NewPreviewScene();
|
||||
try
|
||||
{
|
||||
var path = Parts.Contains(part) ? PartPath(part) : ReusePath(part);
|
||||
var path = Parts.Contains(part) || Props.Contains(part) ? PartPath(part) : ReusePath(part);
|
||||
var go = (GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(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));
|
||||
@@ -403,8 +846,20 @@ public static class BuildHomeCamp
|
||||
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");
|
||||
var groundRenderer = root.transform.Find("Ground").GetComponentInChildren<Renderer>();
|
||||
if (groundRenderer.sharedMaterial.shader.name != "XWorld/XTerrain" || !groundRenderer.receiveShadows)
|
||||
throw new InvalidOperationException("Ground must be tiled XTerrain receiving shadows");
|
||||
var unlitGameplay = root.GetComponentsInChildren<Renderer>(true)
|
||||
.Where(r => r.transform != backdrop && r.sharedMaterial && r.sharedMaterial.shader.name.Contains("Unlit")).Select(r => r.name).ToArray();
|
||||
if (unlitGameplay.Length > 0) throw new InvalidOperationException("Unlit outside backdrop: " + string.Join(",", unlitGameplay));
|
||||
var volume = root.GetComponentInChildren<Volume>();
|
||||
if (!volume || !volume.sharedProfile || !volume.sharedProfile.Has<Tonemapping>()) throw new InvalidOperationException("Missing post Volume");
|
||||
var cards = root.GetComponentsInChildren<Renderer>(true).Count(r => r.sharedMaterial && r.sharedMaterial.shader.name == "Universal Render Pipeline/Simple Lit");
|
||||
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");
|
||||
var tech = "TECH PASS " + string.Join("; ", report) + "; assembledTriangles=" + triangleCount + "; ground XTerrain lit+shadow; litFoliageCards=" + cards
|
||||
+ "; post Volume; fog; ground collision 25/25; unlit quad backdrop only; independent scene; cottage v2 placed as HouseLeft; Studio ground still review-only";
|
||||
const string art = "ART PENDING six-axis review against home_camp.png; see Tools/output/homecamp_review/compare_portrait.png";
|
||||
File.WriteAllText(Path.Combine(Review, "verification.txt"), tech + "\n" + art + "\n");
|
||||
return File.ReadAllText(Path.Combine(Review, "verification.txt"));
|
||||
}
|
||||
public static string OpenForReview()
|
||||
@@ -412,10 +867,18 @@ public static class BuildHomeCamp
|
||||
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 root = scene.GetRootGameObjects().Single(g => g.name == "HomeCamp"); Selection.activeGameObject = root;
|
||||
var camera = root.GetComponentInChildren<Camera>();
|
||||
if (SceneView.lastActiveSceneView != null) SceneView.lastActiveSceneView.AlignViewToObject(camera.transform);
|
||||
return "Opened HomeCamp additively; existing scenes preserved.";
|
||||
}
|
||||
public static string CloseReview()
|
||||
{
|
||||
var scene = SceneManager.GetSceneByPath(ScenePath);
|
||||
if (!scene.IsValid() || !scene.isLoaded) return "HomeCamp not open.";
|
||||
if (scene.isDirty) throw new InvalidOperationException("HomeCamp has unsaved edits; save or discard manually.");
|
||||
EditorSceneManager.CloseScene(scene, true);
|
||||
return "Closed HomeCamp review scene.";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user