Цвет -- в _BaseColor (URP), раньше всё было белым. Настройка 3D-рендерера добавляет его в URP Asset каждого уровня качества (в проекте URP задан только в Quality) + отдельная команда меню. Карта мира: замок со стенами и башнями, города -- домики, лагеря -- шатры с костром, кристаллы точек, рощи. 3D-поселение: здания с двускатными крышами и приметами, ели и лиственные, песчаные берега, снег на скалах. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
289 lines
14 KiB
C#
289 lines
14 KiB
C#
using System.Collections.Generic;
|
||
using TMPro;
|
||
using UnityEngine;
|
||
using UnityEngine.Rendering;
|
||
using UnityEngine.Rendering.Universal;
|
||
|
||
// 3D НА ПРИМИТИВАХ: общие помощники для карты мира и 3D-поселения (переход на 2.5D, 04.11.2026).
|
||
// Материал берётся у встроенного примитива (шейдер URP по умолчанию точно есть в сборке), цвета кешируются.
|
||
// Свои простые формы (конус, пирамида, двускатная крыша) -- для деревьев, шатров, крыш.
|
||
// Подписи (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)
|
||
{
|
||
var probe = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
||
baseMaterial = new Material(probe.GetComponent<Renderer>().sharedMaterial);
|
||
Object.Destroy(probe);
|
||
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;
|
||
}
|
||
}
|