Files
MVP1/Assets/Scripts/WorldMap/Map3D.cs
T

298 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<Color, Material> materials = new Dictionary<Color, Material>();
static Mesh cone, pyramid, roof;
// ---------------- Материалы ----------------
public static Material Mat(Color color)
{
if (baseMaterial == null)
{
// Свой материал на URP Lit (Resources/Map3D/Map3DLit): материал встроенных примитивов в 2D-проекте --
// спрайтовый, без света и без цвета. Нет ассета -- ищем шейдер по имени
var asset = Resources.Load<Material>("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<Renderer>().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<Collider>());
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<Renderer>();
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<MeshFilter>().sharedMesh = mesh;
var r = go.AddComponent<MeshRenderer>();
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<Vector3>();
var t = new List<int>();
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<Vector3> v, List<int> 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<Vector3>();
var t = new List<int>();
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<TextMeshPro>();
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<FaceCamera>();
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<Light>(FindObjectsSortMode.None))
if (l.type == LightType.Directional) { sun = l; break; }
if (sun == null)
{
sun = new GameObject("Sun").AddComponent<Light>();
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;
}
}