2026-11-04 14:55:41 +01:00
using System.Collections.Generic ;
using TMPro ;
using UnityEngine ;
using UnityEngine.Rendering ;
using UnityEngine.Rendering.Universal ;
// 3D НА ПРИМИТИВАХ: общие помощники для карты мира и 3D-поселения (переход на 2.5D, 04.11.2026).
2026-11-04 15:05:12 +01:00
// Материал -- свой, на URP Lit (Resources/Map3D/Map3DLit.mat), цвета кешируются.
2026-11-04 15:03:16 +01:00
// Свои простые формы (конус, пирамида, двускатная крыша) -- для деревьев, шатров, крыш.
2026-11-04 14:55:41 +01:00
// Подписи (TextMeshPro) поворачиваются к камере (FaceCamera).
public static class Map3D
{
static Material baseMaterial ;
static readonly Dictionary < Color , Material > materials = new Dictionary < Color , Material >();
2026-11-04 15:03:16 +01:00
static Mesh cone , pyramid , roof ;
// ---------------- Материалы ----------------
2026-11-04 14:55:41 +01:00
public static Material Mat ( Color color )
{
if ( baseMaterial == null )
{
2026-11-04 15:05:12 +01:00
// Свой материал на 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 будет без света" );
}
2026-11-04 14:55:41 +01:00
materials . Clear ();
}
if (! materials . TryGetValue ( color , out var m ) || m == null )
{
2026-11-04 15:03:16 +01:00
m = new Material ( baseMaterial );
SetColor ( m , color );
if ( m . HasProperty ( "_Smoothness" )) m . SetFloat ( "_Smoothness" , 0.12f ); // матовые, без пластикового блеска
2026-11-04 14:55:41 +01:00
materials [ color ] = m ;
}
return m ;
}
2026-11-04 15:03:16 +01:00
// 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 ;
}
// ---------------- Простые формы ----------------
2026-11-04 14:55:41 +01:00
// Объект на земле: основание 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 ;
}
2026-11-04 15:03:16 +01:00
// Своя форма (конус, пирамида, крыша): основание 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 );
}
// ---------------- Подписи, свет, рендерер ----------------
2026-11-04 14:55:41 +01:00
// Подпись над объектом, всегда лицом к камере
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 ;
}
2026-11-04 15:03:16 +01:00
// Камере -- обычный 3D-рендерер URP (добавляет Tools -> Bogatyri -> 3D-город: создать сцену City3D).
2026-11-04 14:55:41 +01:00
// Нет его -- остаётся рендерер по умолчанию, и в лог пишется, что делать.
public static void UseRenderer3D ( Camera cam )
{
2026-11-04 15:03:16 +01:00
var pipeline = ( GraphicsSettings . currentRenderPipeline ?? QualitySettings . renderPipeline ) as UniversalRenderPipelineAsset ;
2026-11-04 14:55:41 +01:00
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 рисуется без света." );
}
2026-11-04 15:03:16 +01:00
// Солнце, общий свет, небо и дымка вдали (2D-сцены света не имели)
public static void SetUpLook ( Camera cam , float fogStart , float fogEnd )
2026-11-04 14:55:41 +01:00
{
2026-11-04 15:03:16 +01:00
Light sun = null ;
2026-11-04 14:55:41 +01:00
foreach ( var l in Object . FindObjectsByType < Light >( FindObjectsSortMode . None ))
2026-11-04 15:03:16 +01:00
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 ;
2026-11-04 14:55:41 +01:00
sun . shadows = LightShadows . Soft ;
2026-11-04 15:03:16 +01:00
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 ;
}
2026-11-04 14:55:41 +01:00
}
}
// Подпись поворачивается к камере (наклонный вид 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 ;
}
}