using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; namespace ProceduralEditor { // "Set dressing" pass for the PortalTestDestination practice range, run after // SetupPortalTestDestinationScene (which builds the greybox): // - textures the greybox (pale concrete floor, cream plaster walls, concrete cover) with CC0 Poly Haven maps, // swapping every block's cube mesh for one whose UVs are in meters so textures never stretch; // - places CC0/CC-BY props (see Assets/Models/Props/PROPS_CREDITS.txt) with box colliders; // - raises the perimeter walls and builds a pitched warehouse roof (trusses, purlins, skylights); // - lights it like Valorant's range: warm afternoon sun, bright bounce ambient, pendant lamps; // - adds a global post-processing volume with a soft, warm, low-contrast grade. // Safe to re-run: it removes its own previous output first. public static class DressPortalTestDestinationScene { const string ScenePath = "Assets/Scenes/PortalTestDestination.unity"; const string PropsFolder = "Assets/Models/Props"; const string TexFolder = "Assets/Textures/Range"; const string MeshFolder = "Assets/Models/Generated/RangeBlocks"; const string MaterialsFolder = "Assets/Materials"; const string ProfilePath = "Assets/Settings/RangePostProfile.asset"; const string PropsRootName = "Props"; const string LightingRootName = "RangeLighting"; const string RoofRootName = "Roof"; [MenuItem("Tools/Scene/Dress Portal Test Destination (props, textures, lighting)")] public static void Dress() { if (EditorApplication.isPlaying) { Debug.LogError("Dress Portal Test Destination: exit Play Mode first."); return; } var scene = EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Single); var map = GameObject.Find("ShooterMapPrototype"); if (map == null) { Debug.LogError("Dress Portal Test Destination: 'ShooterMapPrototype' not found — run the setup menu first."); return; } foreach (var root in scene.GetRootGameObjects()) if (root.name == PropsRootName || root.name == LightingRootName || root.name == RoofRootName) Object.DestroyImmediate(root); TextureGreybox(map.transform); BuildRoof(); PlaceProps(); SetupLighting(); EditorSceneManager.MarkSceneDirty(scene); EditorSceneManager.SaveScene(scene); AssetDatabase.SaveAssets(); Debug.Log("Portal Test Destination dressed and saved."); } // ---- Greybox textures ----------------------------------------------------------- static Material floorMat, wallMat, boxMat, woodMat, deckMat, steelMat; static void TextureGreybox(Transform map) { // Warehouse palette (matches Valorant's range): pale concrete, cream plaster, concrete cover. // Last args: metres per texture tile, tint, smoothness, normal strength. floorMat = SetupSurface("ShooterProto_Floor", "hangar_concrete_floor", 4f, new Color(1f, 0.97f, 0.92f), 0.3f, 0.8f); wallMat = SetupSurface("ShooterProto_Wall", "clay_plaster", 3f, new Color(1f, 0.93f, 0.82f), 0.1f, 0.9f); boxMat = SetupSurface("ShooterProto_Box", "brushed_concrete", 2f, new Color(0.9f, 0.88f, 0.86f), 0.3f, 0.8f); woodMat = SetupSurface("RangeWood", "rough_wood", 2f, new Color(0.95f, 0.8f, 0.62f), 0.15f, 1f); deckMat = SetupSurface("RangeRoofDeck", "wood_planks", 3f, new Color(0.85f, 0.72f, 0.58f), 0.1f, 1f); steelMat = SetupSurface("RangeSteel", "metal_plate", 2f, new Color(0.45f, 0.48f, 0.52f), 0.5f, 0.6f, metallic: 0.6f); // Perimeter walls go up to the eaves now that there's a roof. foreach (var name in new[] { "Wall_North", "Wall_South", "Wall_East", "Wall_West" }) { var w = map.Find(name); if (w == null) continue; w.localScale = new Vector3(w.localScale.x, EaveHeight, w.localScale.z); w.localPosition = new Vector3(w.localPosition.x, EaveHeight / 2f, w.localPosition.z); } EnsureFolder(MeshFolder); foreach (var mf in map.GetComponentsInChildren()) { var renderer = mf.GetComponent(); if (renderer.sharedMaterial == floorMat || renderer.sharedMaterial == wallMat || renderer.sharedMaterial == boxMat) mf.sharedMesh = GetMeterCube(mf.transform.lossyScale); } } static Material SetupSurface(string materialName, string texName, float metersPerTile, Color tint, float smoothness, float normalStrength, float metallic = 0f) { string path = $"{MaterialsFolder}/{materialName}.mat"; var mat = AssetDatabase.LoadAssetAtPath(path); if (mat == null) { mat = new Material(Shader.Find("Universal Render Pipeline/Lit")); AssetDatabase.CreateAsset(mat, path); } var albedo = AssetDatabase.LoadAssetAtPath($"{TexFolder}/{texName}_diff_1k.jpg"); var normalPath = $"{TexFolder}/{texName}_nor_gl_1k.jpg"; var importer = (TextureImporter)AssetImporter.GetAtPath(normalPath); if (importer != null && importer.textureType != TextureImporterType.NormalMap) { importer.textureType = TextureImporterType.NormalMap; importer.SaveAndReimport(); } var normal = AssetDatabase.LoadAssetAtPath(normalPath); // Mesh UVs are in meters (see GetMeterCube), so tiling = tiles per meter. var tiling = Vector2.one / metersPerTile; mat.SetTexture("_BaseMap", albedo); mat.SetTextureScale("_BaseMap", tiling); mat.SetColor("_BaseColor", tint); mat.SetTexture("_BumpMap", normal); mat.SetFloat("_BumpScale", normalStrength); mat.EnableKeyword("_NORMALMAP"); mat.SetFloat("_Smoothness", smoothness); mat.SetFloat("_Metallic", metallic); EditorUtility.SetDirty(mat); return mat; } static readonly Dictionary meshCache = new Dictionary(); // A unit cube whose UVs are scaled by the block's size, so 1 UV unit = 1 meter on every face. static Mesh GetMeterCube(Vector3 size) { size = new Vector3(Mathf.Abs(size.x), Mathf.Abs(size.y), Mathf.Abs(size.z)); string key = $"Block_{size.x:0.##}x{size.y:0.##}x{size.z:0.##}".Replace(',', '.'); if (meshCache.TryGetValue(key, out var cached) && cached != null) return cached; string path = $"{MeshFolder}/{key}.asset"; var mesh = AssetDatabase.LoadAssetAtPath(path); if (mesh == null) { mesh = BuildMeterCube(size); mesh.name = key; AssetDatabase.CreateAsset(mesh, path); } meshCache[key] = mesh; return mesh; } static Mesh BuildMeterCube(Vector3 s) { var verts = new List(); var normals = new List(); var uvs = new List(); var tris = new List(); // (normal, u axis, v axis, u size, v size) void Face(Vector3 n, Vector3 u, Vector3 v, float uSize, float vSize) { int start = verts.Count; Vector3 c = n * 0.5f; verts.Add(c - u * 0.5f - v * 0.5f); uvs.Add(new Vector2(0, 0)); verts.Add(c + u * 0.5f - v * 0.5f); uvs.Add(new Vector2(uSize, 0)); verts.Add(c + u * 0.5f + v * 0.5f); uvs.Add(new Vector2(uSize, vSize)); verts.Add(c - u * 0.5f + v * 0.5f); uvs.Add(new Vector2(0, vSize)); for (int i = 0; i < 4; i++) normals.Add(n); // Clockwise when viewed from outside (Unity's front face). tris.AddRange(new[] { start, start + 2, start + 1, start, start + 3, start + 2 }); } Face(Vector3.forward, Vector3.left, Vector3.up, s.x, s.y); Face(Vector3.back, Vector3.right, Vector3.up, s.x, s.y); Face(Vector3.right, Vector3.forward, Vector3.up, s.z, s.y); Face(Vector3.left, Vector3.back, Vector3.up, s.z, s.y); Face(Vector3.up, Vector3.right, Vector3.forward, s.x, s.z); Face(Vector3.down, Vector3.right, Vector3.back, s.x, s.z); var mesh = new Mesh(); mesh.SetVertices(verts); mesh.SetNormals(normals); mesh.SetUVs(0, uvs); mesh.SetTriangles(tris, 0); mesh.RecalculateTangents(); mesh.RecalculateBounds(); return mesh; } // ---- Props ---------------------------------------------------------------------- struct Placement { public string prop; public Vector3 pos; public float yaw; public Placement(string prop, float x, float y, float z, float yaw = 0f) { this.prop = prop; pos = new Vector3(x, y, z); this.yaw = yaw; } } // Layout keeps the central street sightlines, the building doors, the targets and the intro // cinematic's dolly path (x -1.2..2.2, z 10..16) clear; props hug walls, corners and alleys. static readonly Placement[] Layout = { // Spawn cover new Placement("ConcreteBarrier", -3.8f, 0, -11.2f, 0), new Placement("ConcreteBarrier", 3.8f, 0, 11.2f, 180), new Placement("Sandbags", 3.6f, 0, -11.4f, 90), new Placement("Sandbags", -3.6f, 0, 11.4f, 90), // Alley cover new Placement("ConcreteBarrier", 7.6f, 0, 3.2f, 90), new Placement("ConcreteBarrier", -7.6f, 0, -3.2f, 90), new Placement("Sandbags", -8.4f, 0, 4.8f, 0), new Placement("Sandbags", 8.4f, 0, -4.8f, 0), // Street clutter new Placement("TrafficCone", 3.8f, 0, -4.4f, 0), new Placement("TrafficCone", 4.3f, 0, -3.6f, 25), new Placement("TrafficCone", -3.9f, 0, 5.2f, -10), new Placement("Pallet", 4.2f, 0, 5.8f, 12), // South-west corner storage new Placement("Barrel", -20.6f, 0, -15.6f, 0), new Placement("Barrel", -19.7f, 0, -15.9f, 40), new Placement("Barrel", -20.4f, 0, -14.7f, 75), new Placement("Crate", -18.4f, 0, -15.8f, 5), new Placement("CubeCrate", -18.4f, 0.9f, -15.8f, 20), new Placement("Pallet", -16.6f, 0, -15.9f, 0), new Placement("Pallet", -16.6f, 0.14f, -15.9f, 6), // North-east corner storage new Placement("Crate", 20.5f, 0, 15.6f, 0), new Placement("Crate", 19.5f, 0, 15.7f, 8), new Placement("Crate", 20.1f, 0.9f, 15.6f, -12), new Placement("Barrel", 17.9f, 0, 15.9f, 0), new Placement("Tires", 16.3f, 0, 15.7f, 0), // Back alleys new Placement("Tires", -20.8f, 0, 4.5f, 90), new Placement("Barrel", -21.1f, 0, -5.4f, 0), new Placement("CubeCrate", -21.0f, 0, -6.4f, 15), new Placement("Tires", 20.8f, 0, -4.5f, 90), new Placement("Barrel", 21.1f, 0, 5.4f, 0), new Placement("Crate", 20.9f, 0, 6.5f, -8), new Placement("Pallet", 20.2f, 0, -12.6f, 90), new Placement("Pallet", -20.2f, 0, 12.6f, 90), }; static void PlaceProps() { var root = new GameObject(PropsRootName).transform; var counts = new Dictionary(); foreach (var p in Layout) { var prefab = AssetDatabase.LoadAssetAtPath($"{PropsFolder}/{p.prop}.fbx"); if (prefab == null) { Debug.LogWarning($"Dress: missing prop {p.prop}.fbx"); continue; } counts.TryGetValue(p.prop, out int n); counts[p.prop] = n + 1; var go = (GameObject)PrefabUtility.InstantiatePrefab(prefab, root); go.name = $"{p.prop}_{n + 1}"; go.transform.SetPositionAndRotation(p.pos, Quaternion.Euler(0, p.yaw, 0)); GameObjectUtility.SetStaticEditorFlags(go, StaticEditorFlags.BatchingStatic | StaticEditorFlags.OccluderStatic | StaticEditorFlags.OccludeeStatic); AddBoxCollider(go); } } // A BoxCollider added next to a MeshFilter auto-fits the mesh bounds (in that object's space), // so bullets and the camera ray hit props properly. static void AddBoxCollider(GameObject go) { var mf = go.GetComponentInChildren(); if (mf != null && mf.GetComponent() == null) mf.gameObject.AddComponent(); } // ---- Roof ----------------------------------------------------------------------- const float EaveHeight = 8f; const float RidgeHeight = 10.5f; const float HalfSpanX = 23f; // perimeter walls sit at x = ±22.5 const float HalfSpanZ = 18f; // and z = ±17.5 static readonly float[] TrussZ = { -15f, -10f, -5f, 0f, 5f, 10f, 15f }; // Pitched warehouse roof in the style of Valorant's range: wooden trusses across the short // span, steel purlins along it, a plank deck with a skylight strip on each slope. // The deck doesn't cast shadows (daylight still reaches the floor, standing in for light // pouring through the skylights); trusses and purlins do, striping the floor with shadows. static void BuildRoof() { var root = new GameObject(RoofRootName).transform; float rise = RidgeHeight - EaveHeight; float rafterLen = Mathf.Sqrt(HalfSpanX * HalfSpanX + rise * rise); float pitch = Mathf.Atan2(rise, HalfSpanX) * Mathf.Rad2Deg; foreach (float z in TrussZ) { var truss = new GameObject($"Truss_{z:+0;-0;0}").transform; truss.SetParent(root, false); truss.localPosition = new Vector3(0, 0, z); Beam(truss, "TieBeam", new Vector3(0, EaveHeight - 0.25f, 0), new Vector3(HalfSpanX * 2f, 0.35f, 0.3f), Quaternion.identity, woodMat); Beam(truss, "KingPost", new Vector3(0, (EaveHeight + RidgeHeight) / 2f - 0.1f, 0), new Vector3(0.3f, rise + 0.3f, 0.3f), Quaternion.identity, woodMat); foreach (int side in new[] { -1, 1 }) { Vector3 mid = new Vector3(side * HalfSpanX / 2f, (EaveHeight + RidgeHeight) / 2f, 0); Beam(truss, side < 0 ? "Rafter_W" : "Rafter_E", mid, new Vector3(rafterLen, 0.35f, 0.3f), Quaternion.Euler(0, 0, -side * pitch), woodMat); // Queen posts + a diagonal strut per side. foreach (float x in new[] { 7.5f, 15f }) { float h = RidgeHeight - rise * (x / HalfSpanX) - (EaveHeight - 0.1f); Beam(truss, "Post", new Vector3(side * x, EaveHeight - 0.1f + h / 2f, 0), new Vector3(0.22f, h, 0.22f), Quaternion.identity, woodMat); } Vector3 a = new Vector3(side * 7.5f, EaveHeight - 0.1f, 0); Vector3 b = new Vector3(side * 1f, RidgeHeight - rise / HalfSpanX - 0.25f, 0); Beam(truss, "Strut", (a + b) / 2f, new Vector3(Vector3.Distance(a, b), 0.2f, 0.2f), Quaternion.FromToRotation(Vector3.right, b - a), woodMat); } } // Steel purlins running the length of the hall, sitting on the rafters. foreach (float x in new[] { -18f, -12f, -6f, 0f, 6f, 12f, 18f }) { float y = RidgeHeight - rise * (Mathf.Abs(x) / HalfSpanX) + 0.2f; Beam(root, $"Purlin_{x:+0;-0;0}", new Vector3(x, y, 0), new Vector3(0.22f, 0.3f, HalfSpanZ * 2f), Quaternion.identity, steelMat); } // Deck panels on each slope, leaving a skylight strip between 3 m and 6 m from the ridge. foreach (int side in new[] { -1, 1 }) { DeckPanel(root, side, 0f, 3f, rise, pitch); DeckPanel(root, side, 6f, HalfSpanX, rise, pitch); foreach (float x in new[] { 3f, 6f }) { float y = RidgeHeight - rise * (x / HalfSpanX) + 0.3f; Beam(root, "SkylightRail", new Vector3(side * x, y, 0), new Vector3(0.18f, 0.18f, HalfSpanZ * 2f), Quaternion.identity, steelMat); } } // Gable walls close the triangles above the north/south walls. foreach (float z in new[] { -17.5f, 17.5f }) Beam(root, "Gable", new Vector3(0, (EaveHeight + RidgeHeight) / 2f + 0.3f, z), new Vector3(HalfSpanX * 2f, rise + 0.6f, 1f), Quaternion.identity, wallMat); } static void DeckPanel(Transform root, int side, float fromX, float toX, float rise, float pitch) { float midX = (fromX + toX) / 2f; float width = (toX - fromX) / Mathf.Cos(pitch * Mathf.Deg2Rad); float y = RidgeHeight - rise * (midX / HalfSpanX) + 0.45f; var deck = Beam(root, "Deck", new Vector3(side * midX, y, 0), new Vector3(width, 0.12f, HalfSpanZ * 2f), Quaternion.Euler(0, 0, -side * pitch), deckMat); deck.GetComponent().shadowCastingMode = ShadowCastingMode.Off; } static GameObject Beam(Transform parent, string name, Vector3 localPos, Vector3 size, Quaternion rot, Material mat, bool meterUV = true) { var go = GameObject.CreatePrimitive(PrimitiveType.Cube); go.name = name; Object.DestroyImmediate(go.GetComponent()); go.transform.SetParent(parent, false); go.transform.localPosition = localPos; go.transform.localRotation = rot; go.transform.localScale = size; if (meterUV) go.GetComponent().sharedMesh = GetMeterCube(size); go.GetComponent().sharedMaterial = mat; GameObjectUtility.SetStaticEditorFlags(go, StaticEditorFlags.BatchingStatic); return go; } // ---- Lighting ------------------------------------------------------------------- static void SetupLighting() { var root = new GameObject(LightingRootName).transform; // Warm, high afternoon sun slanting in through the skylights. var sun = RenderSettings.sun; if (sun != null) { sun.transform.rotation = Quaternion.Euler(52f, 118f, 0f); sun.intensity = 2.1f; sun.color = new Color(1f, 0.92f, 0.8f); sun.shadowStrength = 0.75f; } // Bright warm ambient stands in for bounce light off the cream walls and floor. RenderSettings.ambientMode = AmbientMode.Trilight; RenderSettings.ambientSkyColor = new Color(0.92f, 0.85f, 0.76f); RenderSettings.ambientEquatorColor = new Color(0.82f, 0.72f, 0.6f); RenderSettings.ambientGroundColor = new Color(0.5f, 0.44f, 0.38f); RenderSettings.fog = true; RenderSettings.fogMode = FogMode.Linear; RenderSettings.fogColor = new Color(0.92f, 0.84f, 0.72f); RenderSettings.fogStartDistance = 25f; RenderSettings.fogEndDistance = 140f; DynamicGI.UpdateEnvironment(); // Daytime palette for the sky seen through the skylights. var sky = RenderSettings.skybox; if (sky != null && sky.shader.name == "Skybox/RangeStylizedSky") { sky.SetColor("_ZenithColor", new Color(0.32f, 0.52f, 0.85f)); sky.SetColor("_SkyColor", new Color(0.55f, 0.72f, 0.93f)); sky.SetColor("_HorizonColor", new Color(1f, 0.86f, 0.7f)); sky.SetColor("_GroundColor", new Color(0.82f, 0.74f, 0.64f)); sky.SetColor("_CloudColor", new Color(1f, 0.98f, 0.95f)); sky.SetColor("_CloudShadowColor", new Color(0.72f, 0.74f, 0.82f)); sky.SetFloat("_StarBrightness", 0f); EditorUtility.SetDirty(sky); } // Industrial pendant lamps hanging from the roof over the street and alleys. var glow = GetGlowMaterial(); foreach (float z in new[] { -10f, 0f, 10f }) foreach (float x in new[] { -6f, 0f, 6f }) Pendant(root, new Vector3(x, 0, z), glow, steelMat); // Global post-processing. var volumeGo = new GameObject("PostProcessVolume"); volumeGo.transform.SetParent(root); var volume = volumeGo.AddComponent(); volume.isGlobal = true; volume.priority = 1; volume.sharedProfile = GetPostProfile(); var cam = Camera.main; if (cam != null) { var data = cam.GetUniversalAdditionalCameraData(); data.renderPostProcessing = true; data.antialiasing = AntialiasingMode.SubpixelMorphologicalAntiAliasing; } } static void Pendant(Transform root, Vector3 floorPos, Material glow, Material shade) { const float lampY = 5.6f; var lamp = new GameObject($"Pendant_{floorPos.x:+0;-0;0}_{floorPos.z:+0;-0;0}").transform; lamp.SetParent(root, false); lamp.localPosition = new Vector3(floorPos.x, lampY, floorPos.z); float cordLen = EaveHeight - 0.4f - lampY; Beam(lamp, "Cord", new Vector3(0, cordLen / 2f + 0.15f, 0), new Vector3(0.03f, cordLen, 0.03f), Quaternion.identity, shade, meterUV: false); var shadeGo = GameObject.CreatePrimitive(PrimitiveType.Cylinder); shadeGo.name = "Shade"; Object.DestroyImmediate(shadeGo.GetComponent()); shadeGo.transform.SetParent(lamp, false); shadeGo.transform.localPosition = new Vector3(0, 0.1f, 0); shadeGo.transform.localScale = new Vector3(0.7f, 0.1f, 0.7f); shadeGo.GetComponent().sharedMaterial = shade; var bulb = GameObject.CreatePrimitive(PrimitiveType.Sphere); bulb.name = "Bulb"; Object.DestroyImmediate(bulb.GetComponent()); bulb.transform.SetParent(lamp, false); bulb.transform.localPosition = new Vector3(0, -0.02f, 0); bulb.transform.localScale = new Vector3(0.3f, 0.16f, 0.3f); var br = bulb.GetComponent(); br.sharedMaterial = glow; br.shadowCastingMode = ShadowCastingMode.Off; var lightGo = new GameObject("LampLight"); lightGo.transform.SetParent(lamp, false); lightGo.transform.localPosition = new Vector3(0, -0.3f, 0); var light = lightGo.AddComponent(); light.type = LightType.Point; light.color = new Color(1f, 0.8f, 0.55f); light.range = 10f; light.intensity = 5f; light.shadows = LightShadows.None; } static Material GetGlowMaterial() { string path = $"{MaterialsFolder}/RangeLampGlow.mat"; var mat = AssetDatabase.LoadAssetAtPath(path); if (mat != null) return mat; mat = new Material(Shader.Find("Universal Render Pipeline/Lit")); mat.SetColor("_BaseColor", new Color(1f, 0.85f, 0.6f)); mat.EnableKeyword("_EMISSION"); mat.SetColor("_EmissionColor", new Color(1f, 0.72f, 0.42f) * 6f); mat.globalIlluminationFlags = MaterialGlobalIlluminationFlags.RealtimeEmissive; AssetDatabase.CreateAsset(mat, path); return mat; } // Grading after Valorant's range: bright warm-cream whites, soft (low) contrast, lifted warm // shadows, slightly muted saturation, gentle bloom and almost no vignette. static VolumeProfile GetPostProfile() { var profile = AssetDatabase.LoadAssetAtPath(ProfilePath); if (profile == null) { profile = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(profile, ProfilePath); } T Get() where T : VolumeComponent { if (!profile.TryGet(out T c)) { c = profile.Add(true); c.name = typeof(T).Name; AssetDatabase.AddObjectToAsset(c, profile); } return c; } Get().mode.Override(TonemappingMode.Neutral); var bloom = Get(); bloom.threshold.Override(1.1f); bloom.intensity.Override(0.35f); bloom.scatter.Override(0.6f); bloom.tint.Override(new Color(1f, 0.9f, 0.78f)); var color = Get(); color.postExposure.Override(0.25f); color.contrast.Override(-10f); color.saturation.Override(-12f); color.colorFilter.Override(new Color(1f, 0.97f, 0.92f)); var wb = Get(); wb.temperature.Override(9f); wb.tint.Override(2f); var smh = Get(); smh.shadows.Override(new Vector4(1.02f, 0.98f, 0.94f, 0.04f)); // warm, lifted shadows smh.midtones.Override(new Vector4(1f, 0.99f, 0.96f, 0.02f)); smh.highlights.Override(new Vector4(1f, 0.98f, 0.93f, 0f)); // cream highlights var lgg = Get(); lgg.lift.Override(new Vector4(1f, 0.98f, 0.95f, 0.05f)); // milky lift, as in the reference var vignette = Get(); vignette.intensity.Override(0.12f); vignette.smoothness.Override(0.5f); EditorUtility.SetDirty(profile); return profile; } static void EnsureFolder(string path) { if (AssetDatabase.IsValidFolder(path)) return; string parent = Path.GetDirectoryName(path).Replace('\\', '/'); EnsureFolder(parent); AssetDatabase.CreateFolder(parent, Path.GetFileName(path)); } } }