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:
pelpanagiotis
2026-09-25 08:56:35 +03:00
co-authored by Claude Opus 5.5
parent c674d76549
commit 5fc9ebf1e6
211 changed files with 34492 additions and 177 deletions
@@ -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));
}
}
}