using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; // 3D НА ПРИМИТИВАХ: общие помощники для карты мира и 3D-поселения (переход на 2.5D, 04.11.2026). // Материал -- свой, на URP Lit (Resources/Map3D/Map3DLit.mat), цвета кешируются. // Свои простые формы (конус, пирамида, двускатная крыша) -- для деревьев, шатров, крыш. // Подписи (TextMeshPro) поворачиваются к камере (FaceCamera). public static class Map3D { static Material baseMaterial; static readonly Dictionary materials = new Dictionary(); static Mesh cone, pyramid, roof; // ---------------- Материалы ---------------- public static Material Mat(Color color) { if (baseMaterial == null) { // Свой материал на URP Lit (Resources/Map3D/Map3DLit): материал встроенных примитивов в 2D-проекте -- // спрайтовый, без света и без цвета. Нет ассета -- ищем шейдер по имени var asset = Resources.Load("Map3D/Map3DLit"); var shader = Shader.Find("Universal Render Pipeline/Lit"); baseMaterial = asset != null ? new Material(asset) : shader != null ? new Material(shader) : null; if (baseMaterial == null) { var probe = GameObject.CreatePrimitive(PrimitiveType.Cube); baseMaterial = new Material(probe.GetComponent().sharedMaterial); Object.Destroy(probe); Debug.LogWarning("[3D] Нет материала Resources/Map3D/Map3DLit и шейдера URP Lit -- 3D будет без света"); } materials.Clear(); } if (!materials.TryGetValue(color, out var m) || m == null) { m = new Material(baseMaterial); SetColor(m, color); if (m.HasProperty("_Smoothness")) m.SetFloat("_Smoothness", 0.12f); // матовые, без пластикового блеска materials[color] = m; } return m; } // URP берёт цвет из _BaseColor, встроенный шейдер -- из _Color: ставим оба public static void SetColor(Material m, Color color) { if (m.HasProperty("_BaseColor")) m.SetColor("_BaseColor", color); if (m.HasProperty("_Color")) m.SetColor("_Color", color); } // Материал с текстурой (земля): своя копия, не из кеша public static Material TexturedMat(Texture texture) { var m = new Material(Mat(Color.white)); if (m.HasProperty("_BaseMap")) m.SetTexture("_BaseMap", texture); if (m.HasProperty("_MainTex")) m.SetTexture("_MainTex", texture); return m; } // ---------------- Простые формы ---------------- // Объект на земле: основание size.x * size.y (по X и Z), высота height, низ -- на уровне ground.y public static Renderer Block(Transform parent, string name, PrimitiveType type, Color color, Vector3 ground, Vector2 size, float height, bool collider = false) { var go = GameObject.CreatePrimitive(type); go.name = name; if (!collider) Object.Destroy(go.GetComponent()); go.transform.SetParent(parent, false); // У цилиндра высота -- 2 единицы при масштабе 1 float scaleY = type == PrimitiveType.Cylinder || type == PrimitiveType.Capsule ? height / 2f : height; go.transform.localScale = new Vector3(size.x, scaleY, size.y); go.transform.position = ground + Vector3.up * height / 2f; var r = go.GetComponent(); r.sharedMaterial = Mat(color); return r; } // Своя форма (конус, пирамида, крыша): основание size.x * size.y, высота height, низ -- на ground public static Renderer Shape(Transform parent, string name, Mesh mesh, Color color, Vector3 ground, Vector2 size, float height, float yaw = 0f) { var go = new GameObject(name); go.transform.SetParent(parent, false); go.transform.position = ground; go.transform.rotation = Quaternion.Euler(0, yaw, 0); go.transform.localScale = new Vector3(size.x, height, size.y); go.AddComponent().sharedMesh = mesh; var r = go.AddComponent(); r.sharedMaterial = Mat(color); return r; } // Конус: основание -- круг диаметром 1 на y = 0, вершина -- (0, 1, 0) public static Mesh Cone => cone != null ? cone : cone = BuildCone(10); // Пирамида: основание 1 x 1, вершина -- (0, 1, 0) public static Mesh Pyramid => pyramid != null ? pyramid : pyramid = BuildCone(4, 45f, 0.7071f); // Двускатная крыша: основание 1 x 1, конёк вдоль X на высоте 1 public static Mesh Roof => roof != null ? roof : roof = BuildRoof(); static Mesh BuildCone(int sides, float startDeg = 0f, float radius = 0.5f) { var v = new List(); var t = new List(); for (int i = 0; i < sides; i++) { float a0 = (startDeg + 360f * i / sides) * Mathf.Deg2Rad, a1 = (startDeg + 360f * (i + 1) / sides) * Mathf.Deg2Rad; Vector3 p0 = new Vector3(Mathf.Cos(a0) * radius, 0, Mathf.Sin(a0) * radius); Vector3 p1 = new Vector3(Mathf.Cos(a1) * radius, 0, Mathf.Sin(a1) * radius); TwoSided(v, t, p0, Vector3.up, p1); // бок -- свои вершины: грани с резкими нормалями TwoSided(v, t, p0, p1, Vector3.zero); // дно } var mesh = new Mesh { name = "Cone" + sides, vertices = v.ToArray(), triangles = t.ToArray() }; mesh.RecalculateNormals(); mesh.RecalculateBounds(); return mesh; } // Треугольник с обеих сторон: у каждой стороны свои вершины и своя нормаль static void TwoSided(List v, List t, Vector3 p, Vector3 q, Vector3 r) { int i = v.Count; v.Add(p); v.Add(q); v.Add(r); t.Add(i); t.Add(i + 1); t.Add(i + 2); i = v.Count; v.Add(p); v.Add(r); v.Add(q); t.Add(i); t.Add(i + 1); t.Add(i + 2); } static Mesh BuildRoof() { Vector3 a = new Vector3(-0.5f, 0, -0.5f), b = new Vector3(0.5f, 0, -0.5f), c = new Vector3(0.5f, 0, 0.5f), d = new Vector3(-0.5f, 0, 0.5f); Vector3 r0 = new Vector3(-0.5f, 1, 0), r1 = new Vector3(0.5f, 1, 0); var v = new List(); var t = new List(); void Tri(Vector3 p, Vector3 q, Vector3 r) => TwoSided(v, t, p, q, r); Tri(a, r0, r1); Tri(a, r1, b); // скат к -Z Tri(c, r1, r0); Tri(c, r0, d); // скат к +Z Tri(d, r0, a); Tri(b, r1, c); // торцы Tri(a, b, c); Tri(a, c, d); // дно var mesh = new Mesh { name = "Roof", vertices = v.ToArray(), triangles = t.ToArray() }; mesh.RecalculateNormals(); mesh.RecalculateBounds(); return mesh; } // ---------------- Готовые фигуры ---------------- // Домик: стены w x d (по X и Z), высота стен h, двускатная крыша; поворот yaw public static Transform House(Transform parent, string name, Vector3 ground, float w, float d, float h, Color walls, Color roofColor, float yaw = 0f) { var root = new GameObject(name).transform; root.SetParent(parent, false); root.position = ground; root.rotation = Quaternion.Euler(0, yaw, 0); var body = Block(root, "Walls", PrimitiveType.Cube, walls, ground, new Vector2(w, d), h); body.transform.localPosition = new Vector3(0, h / 2f, 0); body.transform.localRotation = Quaternion.identity; var top = Shape(root, "Roof", Roof, roofColor, ground, new Vector2(w * 1.12f, d * 1.15f), Mathf.Max(0.25f, Mathf.Min(w, d) * 0.55f)); top.transform.localPosition = new Vector3(0, h, 0); top.transform.localRotation = Quaternion.identity; return root; } static readonly Color Trunk = new Color(0.42f, 0.28f, 0.16f); static readonly Color[] Leaves = { new Color(0.2f, 0.46f, 0.2f), new Color(0.26f, 0.52f, 0.22f), new Color(0.32f, 0.55f, 0.2f) }; static readonly Color[] Pine = { new Color(0.12f, 0.36f, 0.22f), new Color(0.16f, 0.4f, 0.24f) }; // Дерево: ель (конусы) или лиственное (шар). variant -- для разнообразия, scale -- размер public static Transform Tree(Transform parent, string name, Vector3 ground, float scale, int variant) { var root = new GameObject(name).transform; root.SetParent(parent, false); root.position = ground; var trunk = Block(root, "Trunk", PrimitiveType.Cylinder, Trunk, ground, Vector2.one * 0.18f * scale, 0.6f * scale); if (variant % 3 == 0) { var crown = Block(root, "Crown", PrimitiveType.Sphere, Leaves[variant % Leaves.Length], ground + Vector3.up * 0.45f * scale, new Vector2(1.0f, 0.95f) * scale, 1.05f * scale); } else { Color c = Pine[variant % Pine.Length]; Shape(root, "Pine1", Cone, c, ground + Vector3.up * 0.35f * scale, Vector2.one * 1.0f * scale, 1.0f * scale); Shape(root, "Pine2", Cone, c, ground + Vector3.up * 0.8f * scale, Vector2.one * 0.72f * scale, 0.85f * scale); } return root; } static readonly Color[] Stones = { new Color(0.55f, 0.55f, 0.56f), new Color(0.5f, 0.48f, 0.45f), new Color(0.62f, 0.6f, 0.58f) }; // Валун: сплюснутый шар, повёрнутый случайно по variant public static Transform Rock(Transform parent, string name, Vector3 ground, float size, int variant) { var root = new GameObject(name).transform; root.SetParent(parent, false); root.position = ground; var r = Block(root, "Stone", PrimitiveType.Sphere, Stones[variant % Stones.Length], ground + Vector3.down * 0.1f * size, new Vector2(size, size * 0.85f), size * 0.65f); r.transform.rotation = Quaternion.Euler(variant * 7 % 20, variant * 53 % 360, 0); if (size > 1.2f) { var small = Block(root, "Stone2", PrimitiveType.Sphere, Stones[(variant + 1) % Stones.Length], ground + new Vector3(size * 0.4f, 0, size * 0.3f), Vector2.one * size * 0.45f, size * 0.35f); } return root; } // Флаг на древке public static void Flag(Transform parent, Vector3 ground, float height, Color color) { Block(parent, "Pole", PrimitiveType.Cylinder, new Color(0.35f, 0.25f, 0.15f), ground, Vector2.one * 0.06f, height); var f = Block(parent, "Flag", PrimitiveType.Cube, color, ground + new Vector3(0.22f, height - 0.32f, 0), new Vector2(0.42f, 0.03f), 0.28f); } // ---------------- Подписи, свет, рендерер ---------------- // Подпись над объектом, всегда лицом к камере public static TextMeshPro Label(Transform parent, string text, Vector3 position, float fontSize) { var go = new GameObject("Label"); go.transform.SetParent(parent, false); go.transform.position = position; var tmp = go.AddComponent(); tmp.text = text; tmp.fontSize = fontSize; tmp.alignment = TextAlignmentOptions.Center; tmp.rectTransform.sizeDelta = new Vector2(6f, 1.6f); tmp.color = Color.white; tmp.outlineWidth = 0.25f; tmp.outlineColor = Color.black; go.AddComponent(); return tmp; } // Камере -- обычный 3D-рендерер URP (добавляет Tools -> Bogatyri -> 3D-город: создать сцену City3D). // Нет его -- остаётся рендерер по умолчанию, и в лог пишется, что делать. public static void UseRenderer3D(Camera cam) { var pipeline = (GraphicsSettings.currentRenderPipeline ?? QualitySettings.renderPipeline) as UniversalRenderPipelineAsset; var data = cam != null ? cam.GetUniversalAdditionalCameraData() : null; if (pipeline == null || data == null) return; var list = pipeline.rendererDataList; for (int i = 0; i < list.Length; i++) if (list[i] is UniversalRendererData) { data.SetRenderer(i); return; } Debug.LogWarning("[3D] В URP нет обычного 3D-рендерера: выполните Tools -> Bogatyri -> 3D-город: создать сцену City3D (он добавится). Без него 3D рисуется без света."); } // Солнце, общий свет, небо и дымка вдали (2D-сцены света не имели) public static void SetUpLook(Camera cam, float fogStart, float fogEnd) { Light sun = null; foreach (var l in Object.FindObjectsByType(FindObjectsSortMode.None)) if (l.type == LightType.Directional) { sun = l; break; } if (sun == null) { sun = new GameObject("Sun").AddComponent(); sun.type = LightType.Directional; } sun.color = new Color(1f, 0.95f, 0.85f); // тёплое солнце sun.intensity = 1.25f; sun.shadows = LightShadows.Soft; sun.shadowStrength = 0.65f; sun.transform.rotation = Quaternion.Euler(48f, -35f, 0f); RenderSettings.ambientMode = AmbientMode.Trilight; // небо голубое, горизонт светлый, снизу -- тёплая земля RenderSettings.ambientSkyColor = new Color(0.62f, 0.72f, 0.86f); RenderSettings.ambientEquatorColor = new Color(0.55f, 0.6f, 0.58f); RenderSettings.ambientGroundColor = new Color(0.35f, 0.32f, 0.25f); Color sky = new Color(0.66f, 0.8f, 0.94f); RenderSettings.fog = true; RenderSettings.fogMode = FogMode.Linear; RenderSettings.fogColor = sky; RenderSettings.fogStartDistance = fogStart; RenderSettings.fogEndDistance = fogEnd; if (cam != null) { cam.clearFlags = CameraClearFlags.SolidColor; cam.backgroundColor = sky; } } } // Подпись поворачивается к камере (наклонный вид 2.5D) public class FaceCamera : MonoBehaviour { static Camera cam; void LateUpdate() { if (cam == null) cam = Camera.main; if (cam != null) transform.rotation = cam.transform.rotation; } }