Add practice range: shooting, AK47, intro cinematic, dressed warehouse scene
- PortalTestDestination practice range: Valorant-style hitscan rifle (recoil, spray/bloom, reload, controller support), debug HUD, practice-dummy targets with head/body hitboxes, hit/death/respawn animations, procedural SFX/FX - AK47 model (Quaternius, CC0) held via Animator IK, with fire/reload/equip anims - Intro cinematic (camera shots, title card, target beam-in, GO hand-off) - Scene dressing editor pass: CC0 textures, props, warehouse roof, lighting and post-processing; custom stylized skybox shader - CharacterMovement: camera recoil, shoulder offset, cinematic control lock - Unity CLI pipeline package; plus in-progress chat/Rive/player changes Asset credits: Assets/Models/Props/PROPS_CREDITS.txt (3 CC-BY 3.0 props), Assets/Textures/Range/TEXTURES_LICENSE.txt, Assets/Resources/*/..._LICENSE.txt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c674d76549
commit
5fc9ebf1e6
@@ -0,0 +1,553 @@
|
||||
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<MeshFilter>())
|
||||
{
|
||||
var renderer = mf.GetComponent<MeshRenderer>();
|
||||
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<Material>(path);
|
||||
if (mat == null)
|
||||
{
|
||||
mat = new Material(Shader.Find("Universal Render Pipeline/Lit"));
|
||||
AssetDatabase.CreateAsset(mat, path);
|
||||
}
|
||||
|
||||
var albedo = AssetDatabase.LoadAssetAtPath<Texture2D>($"{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<Texture2D>(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<string, Mesh> meshCache = new Dictionary<string, Mesh>();
|
||||
|
||||
// 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<Mesh>(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<Vector3>();
|
||||
var normals = new List<Vector3>();
|
||||
var uvs = new List<Vector2>();
|
||||
var tris = new List<int>();
|
||||
|
||||
// (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<string, int>();
|
||||
foreach (var p in Layout)
|
||||
{
|
||||
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>($"{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<MeshFilter>();
|
||||
if (mf != null && mf.GetComponent<Collider>() == null)
|
||||
mf.gameObject.AddComponent<BoxCollider>();
|
||||
}
|
||||
|
||||
// ---- 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<MeshRenderer>().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<Collider>());
|
||||
go.transform.SetParent(parent, false);
|
||||
go.transform.localPosition = localPos;
|
||||
go.transform.localRotation = rot;
|
||||
go.transform.localScale = size;
|
||||
if (meterUV) go.GetComponent<MeshFilter>().sharedMesh = GetMeterCube(size);
|
||||
go.GetComponent<MeshRenderer>().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>();
|
||||
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<Collider>());
|
||||
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<MeshRenderer>().sharedMaterial = shade;
|
||||
|
||||
var bulb = GameObject.CreatePrimitive(PrimitiveType.Sphere);
|
||||
bulb.name = "Bulb";
|
||||
Object.DestroyImmediate(bulb.GetComponent<Collider>());
|
||||
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<MeshRenderer>();
|
||||
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>();
|
||||
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<Material>(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<VolumeProfile>(ProfilePath);
|
||||
if (profile == null)
|
||||
{
|
||||
profile = ScriptableObject.CreateInstance<VolumeProfile>();
|
||||
AssetDatabase.CreateAsset(profile, ProfilePath);
|
||||
}
|
||||
|
||||
T Get<T>() where T : VolumeComponent
|
||||
{
|
||||
if (!profile.TryGet(out T c))
|
||||
{
|
||||
c = profile.Add<T>(true);
|
||||
c.name = typeof(T).Name;
|
||||
AssetDatabase.AddObjectToAsset(c, profile);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
Get<Tonemapping>().mode.Override(TonemappingMode.Neutral);
|
||||
|
||||
var bloom = Get<Bloom>();
|
||||
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<ColorAdjustments>();
|
||||
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<WhiteBalance>();
|
||||
wb.temperature.Override(9f);
|
||||
wb.tint.Override(2f);
|
||||
|
||||
var smh = Get<ShadowsMidtonesHighlights>();
|
||||
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<LiftGammaGain>();
|
||||
lgg.lift.Override(new Vector4(1f, 0.98f, 0.95f, 0.05f)); // milky lift, as in the reference
|
||||
|
||||
var vignette = Get<Vignette>();
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 86217ae09af2f2941a17242aad0dcacd
|
||||
@@ -0,0 +1,224 @@
|
||||
using Mirror;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
namespace ProceduralEditor
|
||||
{
|
||||
// Rebuilds PortalTestDestination.unity as a small greybox shooter-map prototype:
|
||||
// a walled arena with a central street flanked by two rows of walkable buildings,
|
||||
// in the style of a tactical-shooter blockout pass.
|
||||
public static class SetupPortalTestDestinationScene
|
||||
{
|
||||
const string ScenePath = "Assets/Scenes/PortalTestDestination.unity";
|
||||
const string MaterialsFolder = "Assets/Materials";
|
||||
const string SkyMaterialPath = "Assets/Materials/RangeSky.mat";
|
||||
|
||||
static readonly Color FloorColor = new Color(0.55f, 0.55f, 0.55f);
|
||||
static readonly Color WallColor = new Color(0.28f, 0.28f, 0.3f);
|
||||
static readonly Color BoxColor = new Color(0.76f, 0.58f, 0.34f);
|
||||
|
||||
[MenuItem("Tools/Scene/Setup Portal Test Destination Shooter Map")]
|
||||
public static void Setup()
|
||||
{
|
||||
if (EditorApplication.isPlaying)
|
||||
{
|
||||
Debug.LogError("Setup Portal Test Destination: exit Play Mode first — EditorSceneManager.OpenScene() can't run during Play Mode.");
|
||||
return;
|
||||
}
|
||||
|
||||
var scene = EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Single);
|
||||
|
||||
// Clear out the old studio/pedestal content (Geometry, Lighting rig, post-process
|
||||
// volume, probe volume data, static lighting sky) but keep Main Camera and reconfigure it.
|
||||
foreach (var existingRoot in scene.GetRootGameObjects())
|
||||
{
|
||||
if (existingRoot.name == "Main Camera") continue;
|
||||
Object.DestroyImmediate(existingRoot);
|
||||
}
|
||||
|
||||
var floorMat = GetOrCreateMaterial("ShooterProto_Floor", FloorColor);
|
||||
var wallMat = GetOrCreateMaterial("ShooterProto_Wall", WallColor);
|
||||
var boxMat = GetOrCreateMaterial("ShooterProto_Box", BoxColor);
|
||||
|
||||
SetupCamera();
|
||||
SetupLighting();
|
||||
|
||||
var root = new GameObject("ShooterMapPrototype");
|
||||
|
||||
CreateBlock(root.transform, "Floor", new Vector3(0, -0.25f, 0), new Vector3(44, 0.5f, 34), floorMat);
|
||||
|
||||
// Perimeter walls (floor spans x:[-22,22], z:[-17,17])
|
||||
CreateBlock(root.transform, "Wall_North", new Vector3(0, 2, 17.5f), new Vector3(46, 4, 1), wallMat);
|
||||
CreateBlock(root.transform, "Wall_South", new Vector3(0, 2, -17.5f), new Vector3(46, 4, 1), wallMat);
|
||||
CreateBlock(root.transform, "Wall_East", new Vector3(22.5f, 2, 0), new Vector3(1, 4, 34), wallMat);
|
||||
CreateBlock(root.transform, "Wall_West", new Vector3(-22.5f, 2, 0), new Vector3(1, 4, 34), wallMat);
|
||||
|
||||
var buildingsRoot = new GameObject("Buildings").transform;
|
||||
buildingsRoot.SetParent(root.transform);
|
||||
|
||||
// A central street (x: -4..4) runs the full length of the map between two spawns.
|
||||
// Three buildings line each side of it, each walkable with a door facing the street
|
||||
// and a door facing the back alley, so players can cut through or around them.
|
||||
const float buildingWidth = 6f;
|
||||
const float buildingDepth = 6f;
|
||||
const float wallHeight = 3f;
|
||||
const float wallThickness = 0.5f;
|
||||
const float doorWidth = 2.2f;
|
||||
|
||||
float[] rowZ = { -9f, 0f, 9f };
|
||||
for (int i = 0; i < rowZ.Length; i++)
|
||||
{
|
||||
// Left row: door faces east (street), back door faces west (outer alley)
|
||||
CreateBuilding(buildingsRoot, $"Building_L{i + 1}", new Vector3(-13, 0, rowZ[i]),
|
||||
buildingWidth, buildingDepth, wallHeight, wallThickness,
|
||||
doorEast: true, doorWest: true, doorWidth, wallMat);
|
||||
|
||||
// Right row: mirrored
|
||||
CreateBuilding(buildingsRoot, $"Building_R{i + 1}", new Vector3(13, 0, rowZ[i]),
|
||||
buildingWidth, buildingDepth, wallHeight, wallThickness,
|
||||
doorEast: true, doorWest: true, doorWidth, wallMat);
|
||||
}
|
||||
|
||||
// A couple of symmetric crates in the open street for players crossing between
|
||||
// the two building rows to duck behind.
|
||||
var streetCoverRoot = new GameObject("StreetCover").transform;
|
||||
streetCoverRoot.SetParent(root.transform);
|
||||
CreateBlock(streetCoverRoot, "StreetCover_1", new Vector3(-2, 1, -4), new Vector3(2, 2, 2), boxMat);
|
||||
CreateBlock(streetCoverRoot, "StreetCover_2", new Vector3(2, 1, 4), new Vector3(2, 2, 2), boxMat);
|
||||
|
||||
SetupSpawnPoint("SpawnPoint_A", new Vector3(0, 0, -14));
|
||||
SetupSpawnPoint("SpawnPoint_B", new Vector3(0, 0, 14));
|
||||
|
||||
EditorSceneManager.MarkSceneDirty(scene);
|
||||
Debug.Log("Portal Test Destination rebuilt as a shooter-map prototype. Review it in the Scene view, then save (Ctrl+S).");
|
||||
}
|
||||
|
||||
static void SetupCamera()
|
||||
{
|
||||
var cam = Camera.main;
|
||||
if (cam == null)
|
||||
{
|
||||
var go = new GameObject("Main Camera", typeof(Camera), typeof(AudioListener));
|
||||
go.tag = "MainCamera";
|
||||
cam = go.GetComponent<Camera>();
|
||||
}
|
||||
|
||||
// Overview framing for the editor / before a local player's camera gets reparented
|
||||
// onto them at runtime (see PlayerScript.OnStartLocalPlayer).
|
||||
cam.transform.position = new Vector3(0, 14, -26);
|
||||
cam.transform.rotation = Quaternion.Euler(24f, 0f, 0f);
|
||||
cam.fieldOfView = 60f;
|
||||
cam.clearFlags = CameraClearFlags.Skybox;
|
||||
cam.nearClipPlane = 0.1f;
|
||||
cam.farClipPlane = 200f;
|
||||
|
||||
if (cam.GetComponent<AudioListener>() == null)
|
||||
cam.gameObject.AddComponent<AudioListener>();
|
||||
}
|
||||
|
||||
static void SetupLighting()
|
||||
{
|
||||
// Low dusk sun in the north-west; the RangeSky skybox draws its disc from this light.
|
||||
var go = new GameObject("Directional Light", typeof(Light));
|
||||
var light = go.GetComponent<Light>();
|
||||
light.type = LightType.Directional;
|
||||
go.transform.rotation = Quaternion.Euler(20f, 120f, 0f);
|
||||
light.color = new Color(1f, 0.82f, 0.62f);
|
||||
light.intensity = 1.3f;
|
||||
light.shadows = LightShadows.Soft;
|
||||
RenderSettings.sun = light;
|
||||
|
||||
var sky = AssetDatabase.LoadAssetAtPath<Material>(SkyMaterialPath);
|
||||
if (sky != null) RenderSettings.skybox = sky;
|
||||
else Debug.LogWarning($"Setup Portal Test Destination: {SkyMaterialPath} not found; keeping the current skybox.");
|
||||
|
||||
// Ambient and fog pick up the sky's palette (indigo above, warm horizon).
|
||||
RenderSettings.ambientMode = AmbientMode.Trilight;
|
||||
RenderSettings.ambientSkyColor = new Color(0.42f, 0.40f, 0.62f);
|
||||
RenderSettings.ambientEquatorColor = new Color(0.62f, 0.45f, 0.42f);
|
||||
RenderSettings.ambientGroundColor = new Color(0.16f, 0.14f, 0.18f);
|
||||
RenderSettings.fog = true;
|
||||
RenderSettings.fogMode = FogMode.Linear;
|
||||
RenderSettings.fogColor = new Color(0.55f, 0.42f, 0.48f);
|
||||
RenderSettings.fogStartDistance = 30f;
|
||||
RenderSettings.fogEndDistance = 110f;
|
||||
DynamicGI.UpdateEnvironment();
|
||||
}
|
||||
|
||||
static void SetupSpawnPoint(string name, Vector3 position)
|
||||
{
|
||||
var go = new GameObject(name, typeof(NetworkStartPosition));
|
||||
go.transform.position = position;
|
||||
}
|
||||
|
||||
// Builds a walkable, roofless room: solid North/South walls and East/West walls that
|
||||
// can each optionally have a centered door gap.
|
||||
static void CreateBuilding(Transform parent, string name, Vector3 center, float width, float depth,
|
||||
float wallHeight, float wallThickness, bool doorEast, bool doorWest, float doorWidth, Material material)
|
||||
{
|
||||
var buildingRoot = new GameObject(name).transform;
|
||||
buildingRoot.SetParent(parent, false);
|
||||
buildingRoot.localPosition = center;
|
||||
|
||||
float halfWidth = width / 2f;
|
||||
float halfDepth = depth / 2f;
|
||||
float wallY = wallHeight / 2f;
|
||||
|
||||
// North/South walls run along X; extend by wallThickness so they close the corners
|
||||
// against the East/West walls instead of leaving a gap.
|
||||
CreateBlock(buildingRoot, "Wall_N", new Vector3(0, wallY, halfDepth), new Vector3(width + wallThickness, wallHeight, wallThickness), material);
|
||||
CreateBlock(buildingRoot, "Wall_S", new Vector3(0, wallY, -halfDepth), new Vector3(width + wallThickness, wallHeight, wallThickness), material);
|
||||
|
||||
CreateSideWall(buildingRoot, "Wall_E", new Vector3(halfWidth, wallY, 0), depth, wallThickness, wallHeight, doorEast, doorWidth, material);
|
||||
CreateSideWall(buildingRoot, "Wall_W", new Vector3(-halfWidth, wallY, 0), depth, wallThickness, wallHeight, doorWest, doorWidth, material);
|
||||
}
|
||||
|
||||
// An East/West wall runs along Z; a door splits it into two segments with a gap at the center.
|
||||
static void CreateSideWall(Transform parent, string name, Vector3 center, float depth, float thickness,
|
||||
float height, bool hasDoor, float doorWidth, Material material)
|
||||
{
|
||||
if (!hasDoor)
|
||||
{
|
||||
CreateBlock(parent, name, center, new Vector3(thickness, height, depth), material);
|
||||
return;
|
||||
}
|
||||
|
||||
float segmentLength = (depth - doorWidth) / 2f;
|
||||
float segmentOffset = doorWidth / 2f + segmentLength / 2f;
|
||||
CreateBlock(parent, name + "_A", center + new Vector3(0, 0, -segmentOffset), new Vector3(thickness, height, segmentLength), material);
|
||||
CreateBlock(parent, name + "_B", center + new Vector3(0, 0, segmentOffset), new Vector3(thickness, height, segmentLength), material);
|
||||
}
|
||||
|
||||
static void CreateBlock(Transform parent, string name, Vector3 position, Vector3 size, Material material)
|
||||
{
|
||||
var go = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
||||
go.name = name;
|
||||
go.transform.SetParent(parent, false);
|
||||
go.transform.localPosition = position;
|
||||
go.transform.localScale = size;
|
||||
go.GetComponent<MeshRenderer>().sharedMaterial = material;
|
||||
}
|
||||
|
||||
static Material GetOrCreateMaterial(string name, Color color)
|
||||
{
|
||||
string path = $"{MaterialsFolder}/{name}.mat";
|
||||
var existing = AssetDatabase.LoadAssetAtPath<Material>(path);
|
||||
if (existing != null)
|
||||
{
|
||||
existing.color = color;
|
||||
return existing;
|
||||
}
|
||||
|
||||
if (!AssetDatabase.IsValidFolder(MaterialsFolder))
|
||||
AssetDatabase.CreateFolder("Assets", "Materials");
|
||||
|
||||
var shader = Shader.Find("Universal Render Pipeline/Lit");
|
||||
var mat = new Material(shader) { color = color };
|
||||
AssetDatabase.CreateAsset(mat, path);
|
||||
AssetDatabase.SaveAssets();
|
||||
return mat;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4557b29f669ca924e8fb41557e34de70
|
||||
Reference in New Issue
Block a user