当前位置: 首页 > news >正文

Evolution_07_环境

环境

一、分区

1、在 Scripts 文件夹下新建 Common 文件夹,用于存放通用脚本

2、创建 SpawnRegion.cs(通用分区数据类)

using System;
using UnityEngine;[System.Serializable]
public class SpawnRegion
{[Header("区域位置与大小")]public Vector2 center; // XZ平面的中心坐标public float width = 50f; // X轴方向长度public float length = 50f; // Z轴方向长度[Header("簇生成参数")][Range(5, 30)] public int clusterCount = 10; // 簇的数量[Range(3, 20)] public int minPerCluster = 5; // 每个簇的最小实例数[Range(3, 20)] public int maxPerCluster = 10; // 每个簇的最大实例数// 为不同类型提供默认配置(方便初始化)public static SpawnRegion GetGrassDefault(){return new SpawnRegion{width = 50f,length = 50f,clusterCount = 15,minPerCluster = 10,maxPerCluster = 15};}public static SpawnRegion GetRockDefault(){return new SpawnRegion{width = 50f,length = 50f,clusterCount = 10,minPerCluster = 3,maxPerCluster = 8};}public static SpawnRegion GetRockPileDefault(){return new SpawnRegion{width = 50f,length = 50f,clusterCount = 8,minPerCluster = 2,maxPerCluster = 5};}
}

3、创建 SpawnRegionUtility.cs(通用分区工具类)

using UnityEngine;public static class SpawnRegionUtility
{// 生成簇中心(所有脚本共用的逻辑)public static Vector2[] GenerateClusterCenters(SpawnRegion region){Vector2[] centers = new Vector2[region.clusterCount];float cellWidth = region.width / Mathf.Sqrt(region.clusterCount);float cellLength = region.length / Mathf.Sqrt(region.clusterCount);for (int i = 0; i < region.clusterCount; i++){float x = region.center.x + Random.Range(-region.width / 2 + cellWidth / 2,region.width / 2 - cellWidth / 2);float y = region.center.y + Random.Range(-region.length / 2 + cellLength / 2,region.length / 2 - cellLength / 2);centers[i] = new Vector2(x, y);}return centers;}// 绘制Gizmos(所有脚本共用的调试逻辑)public static void DrawRegionGizmos(SpawnRegion region, Color color, bool showClusterCenters = false){Gizmos.color = color;Vector3 center = new Vector3(region.center.x, 0, region.center.y);Vector3 size = new Vector3(region.width, 1, region.length);Gizmos.DrawWireCube(center, size);if (showClusterCenters && Application.isPlaying){Vector2[] clusters = GenerateClusterCenters(region);foreach (Vector2 point in clusters){Gizmos.DrawSphere(new Vector3(point.x, 0, point.y), 0.5f);}}}
}
二、地形细节参数

(1) 降低Detail Resolution Per Patch(32→16),减少内存占用

(2) 其他参数

① Tree & Detail Objects 中Detail Distance”(细节对象的可见距离阈值‌):改为80

② Wind Settings for Grass (On Terrain Data) 中 Speed改为0.4(减少硬件负载与整体性能表现)

三、花草树木
1、模型

(1) 地址

"D:\Unity\Using\花草树木石\Flower.unitypackage"
"D:\Unity\Using\花草树木石\Tree.unitypackage"

(2) 导入到 Assets/ImportedAssets/Models

2、花草

(1) Create Empty,命名为GrassSpawner,转移到 Environment下(作为Environment的子物体)

(2) 给 GrassSpawner 添加 Assets/Scripts/Environment/GrassSpawner.cs 组件

using UnityEngine;
using System.Collections;public class GrassSpawner : MonoBehaviour
{[Header("Base Settings")][SerializeField] private Terrain terrain;[SerializeField] private GameObject[] grassPrefabs;[SerializeField] private SpawnRegion[] regions; // 通用分区[Header("Cluster Settings")][SerializeField] private float clusterRadius = 3f;[SerializeField][Range(0f, 1f)] private float mixChance = 0.5f;[SerializeField][Range(1, 5)] private int maxMixedTypes = 3;[Header("Randomization")][SerializeField] private Vector2 scaleRange = new Vector2(0.8f, 1.2f);[SerializeField] private bool randomRotation = true;[Header("Debug")][SerializeField] private bool showGizmos = true;[SerializeField] private bool showClusterCenters = true;// 初始化默认区域(Reset时调用)void Reset(){if (regions == null || regions.Length == 0){regions = new SpawnRegion[] { SpawnRegion.GetGrassDefault() };}}void Start(){StartCoroutine(SpawnCoroutine());}bool ValidateSettings(){if (terrain == null) terrain = Terrain.activeTerrain;if (terrain == null || grassPrefabs == null || grassPrefabs.Length == 0){Debug.LogError("Missing terrain or grass prefabs!");return false;}return true;}IEnumerator SpawnCoroutine(){if (!ValidateSettings()) yield break;GameObject grassParent = new GameObject("Spawned_Grasses");foreach (SpawnRegion region in regions){// 用工具类生成簇中心Vector2[] clusterCenters = SpawnRegionUtility.GenerateClusterCenters(region);foreach (Vector2 center in clusterCenters){int mainType = Random.Range(0, grassPrefabs.Length);int[] mixedTypes = GetMixedTypes(mainType);yield return StartCoroutine(SpawnCluster(center, mainType, mixedTypes,Random.Range(region.minPerCluster, region.maxPerCluster + 1),grassParent.transform));}}}IEnumerator SpawnCluster(Vector2 center, int mainType, int[] mixedTypes, int count, Transform parent){for (int i = 0; i < count; i++){int prefabIndex = (mixedTypes.Length > 0 && Random.value < mixChance) ?mixedTypes[Random.Range(0, mixedTypes.Length)] :mainType;Vector2 randomOffset = Random.insideUnitCircle * clusterRadius;Vector3 worldPos = new Vector3(center.x + randomOffset.x,0,center.y + randomOffset.y);worldPos.y = terrain.SampleHeight(worldPos) + terrain.transform.position.y;CreateGrassInstance(grassPrefabs[prefabIndex], worldPos, parent);if (i % 3 == 0) yield return null;}}int[] GetMixedTypes(int mainType){if (grassPrefabs.Length <= 1 || Random.value > mixChance)return new int[0];int mixCount = Random.Range(1, Mathf.Min(maxMixedTypes, grassPrefabs.Length));System.Collections.Generic.List<int> types = new System.Collections.Generic.List<int>();while (types.Count < mixCount){int type = Random.Range(0, grassPrefabs.Length);if (type != mainType && !types.Contains(type))types.Add(type);}return types.ToArray();}void CreateGrassInstance(GameObject prefab, Vector3 position, Transform parent){Quaternion rotation = randomRotation ?Quaternion.Euler(0, Random.Range(0, 360), 0) :Quaternion.identity;GameObject instance = Instantiate(prefab, position, rotation, parent);instance.transform.localScale = Vector3.one * Random.Range(scaleRange.x, scaleRange.y);}void OnDrawGizmosSelected(){if (!showGizmos) return;foreach (SpawnRegion region in regions){SpawnRegionUtility.DrawRegionGizmos(region, Color.green, showClusterCenters);}}
}

(3) 赋值

① Transform

② 预制体

③ 区域

(4) chabrs

(5) 灌木

① Bush_Red

给预制体添加胶囊碰撞体

② Shrub

3、树

修改:

(1) Conifer:

① 设置Static(遮挡体和被遮挡体静态)

② Transform设置

(2) Pine_A、Pine_B、Pine_C、Pine_D

① 设置Static(遮挡体和被遮挡体静态)

② Transform设置

(3) American Elm_Fall-corona

① 在预制体中给各子物体添加胶囊碰撞体

② 注意:不设置Static(遮挡体和被遮挡体静态)

③ Transform设置Position.y改为-0.93

(4) Dab

① 预制体中,添加 碰撞体 组件,球形碰撞体的Center.z改为32

② 设置Static(遮挡体和被遮挡体静态)

③ Transform设置

(5) Cypress_Forest_Desktop

① 设置Static(遮挡体和被遮挡体静态)

② Transform设置

(6) Cherry_shrub_01

四、石子

1、导入模型"D:\Unity\Using\花草树木石\Rock.unitypackage"

2、Create Empty,重命名为"RockSpawner",作为子物体转移到Environment下

3、为 RockSpawner添加 Assets/Scripts/Environment/StoneSpawner.cs

using UnityEngine;
using System.Collections;public class StoneSpawner : MonoBehaviour
{[Header("Base Settings")][SerializeField] private Terrain terrain;[SerializeField] private GameObject[] rockPilePrefabs; // "石子堆"预制体[Header("Spawn Regions")][SerializeField] private SpawnRegion[] regions;[Header("Cluster Settings")][SerializeField] private float clusterRadius = 8f; // 增大簇半径[SerializeField][Range(0f, 1f)] private float mixChance = 0.2f; // 降低混合概率(避免类型过多)[SerializeField][Range(1, 2)] private int maxMixedTypes = 1; // 减少混合类型[Header("Randomization")][SerializeField] private Vector2 scaleRange = new Vector2(0.9f, 1.3f); // 缩小缩放范围[SerializeField] private bool randomRotation = true;[Header("Terrain Alignment")][SerializeField] private bool alignToTerrain = true;[SerializeField] private LayerMask terrainLayerMask = 1; // 默认层[Header("Debug")][SerializeField] private bool showGizmos = true;[SerializeField] private bool showClusterCenters = true;void Reset(){if (terrain == null)terrain = Terrain.activeTerrain;if (regions == null || regions.Length == 0){regions = new SpawnRegion[] {new SpawnRegion {width = 50f,length = 50f,clusterCount = 8,minPerCluster = 2,maxPerCluster = 5}};}}void Start(){StartCoroutine(SpawnCoroutine());}bool ValidateSettings(){if (terrain == null) terrain = Terrain.activeTerrain;if (terrain == null || rockPilePrefabs == null || rockPilePrefabs.Length == 0){Debug.LogError("No valid terrain or rock pile prefabs found!");return false;}return true;}IEnumerator SpawnCoroutine(){if (!ValidateSettings()) yield break;GameObject rockParent = new GameObject("Spawned_RockPiles");rockParent.transform.parent = transform;foreach (SpawnRegion region in regions){if (region == null) continue; // 跳过空区域Vector2[] clusterCenters = SpawnRegionUtility.GenerateClusterCenters(region);foreach (Vector2 center in clusterCenters){int mainType = Random.Range(0, rockPilePrefabs.Length);int[] mixedTypes = GetMixedTypes(mainType);yield return StartCoroutine(SpawnCluster(center, mainType, mixedTypes,Random.Range(region.minPerCluster, region.maxPerCluster + 1),rockParent.transform));}}}IEnumerator SpawnCluster(Vector2 center, int mainType, int[] mixedTypes, int count, Transform parent){for (int i = 0; i < count; i++){int prefabIndex = (mixedTypes.Length > 0 && Random.value < mixChance)? mixedTypes[Random.Range(0, mixedTypes.Length)]: mainType;Vector2 randomOffset = Random.insideUnitCircle * clusterRadius;Vector3 worldPos = new Vector3(center.x + randomOffset.x,0,center.y + randomOffset.y);// 获取准确的地形高度worldPos.y = terrain.SampleHeight(worldPos) + terrain.transform.position.y;CreateRockPileInstance(rockPilePrefabs[prefabIndex], worldPos, parent);if (i % 2 == 0) yield return null; // 每生成2个暂停(因预制体较大)}}int[] GetMixedTypes(int mainType){if (rockPilePrefabs.Length <= 1 || Random.value > mixChance)return new int[0];int mixCount = Random.Range(1, Mathf.Min(maxMixedTypes, rockPilePrefabs.Length));System.Collections.Generic.List<int> types = new System.Collections.Generic.List<int>();while (types.Count < mixCount){int type = Random.Range(0, rockPilePrefabs.Length);if (type != mainType && !types.Contains(type))types.Add(type);}return types.ToArray();}void CreateRockPileInstance(GameObject prefab, Vector3 position, Transform parent){Quaternion rotation = randomRotation? Quaternion.Euler(Random.Range(-5f, 5f),   // 轻微X轴旋转Random.Range(0, 360f),   // Y轴全旋转Random.Range(-5f, 5f)    // 轻微Z轴旋转): Quaternion.identity;GameObject instance = Instantiate(prefab, position, rotation, parent);instance.transform.localScale = Vector3.one * Random.Range(scaleRange.x, scaleRange.y);if (alignToTerrain){AlignToTerrain(instance.transform);}}void AlignToTerrain(Transform obj){Vector3 rayOrigin = obj.position + Vector3.up * 10f;RaycastHit hit;if (Physics.Raycast(rayOrigin, Vector3.down, out hit, 20f, terrainLayerMask)){// 贴合地形法线obj.rotation = Quaternion.FromToRotation(Vector3.up, hit.normal) * obj.rotation;// 微调位置确保贴合obj.position = hit.point;}else{// 备用:使用地形高度采样Vector3 terrainPos = obj.position;terrainPos.y = terrain.SampleHeight(obj.position) + terrain.transform.position.y;obj.position = terrainPos;}}void OnDrawGizmosSelected(){if (!showGizmos || regions == null) return;foreach (SpawnRegion region in regions){if (region != null) // 添加空值检查{SpawnRegionUtility.DrawRegionGizmos(region, Color.gray, showClusterCenters);}}}
}

4、石

(1) 向场景添加Rock和Rock1

(2) 原有的碰撞体,勾选 Convex,或直接使用Sphere碰撞体如下:

5、结构:注意,禁用或删除 Particle_Cloud_Atmos_A

五、启用Occlusion Culling

1、标记静态物体

(1) 将Environment下的四个地形标记为Static:Inspector面板右上角勾选 ‌Static

(2) 树木标记如步骤四

2、创建并设置 Occlusion Area

(1) 通过顶部菜单栏:‌Window > Rendering > Occlusion Culling‌,切换到‌Object选项卡

(2) 点击Occlusion Areas,点击弹出的Create New右侧的Occlusion Area,勾选Is View Volume

(3) 点击Hierarchy面板上的Occlusion Area,切换到Inspector面板,设置(覆盖整个地形)

3.3 烘焙遮挡剔除数据

(1) 打开 Occlusion Culling 窗口,切换到 Bake 选项卡

(2) 设置烘焙参数(平衡性能和效果)

参数推荐值说明
Smallest Occluder10 米过滤过小的遮挡物(如小石子、草,减少无效计算)
Smallest Hole2 米过滤过小的孔洞(如地形缝隙,避免误判为 “可穿透”)
Backface Threshold95控制背面剔除敏感度(值越高,背面剔除越严格,性能越好)

(3) 执行烘焙:点击 Bake 按钮

3.4 验证遮挡效果

(1) 切换到 Visualization 选项卡,选择场景中的主摄像机(需确保摄像机有 Camera 组件)

(2) 可视化遮挡效果:点击 Visualize 按钮,在 Scene 视图中

  • 绿色区域:当前摄像机可见的范围(未被遮挡)。
  • 灰色区域:被遮挡剔除的范围(不会被渲染)
六、天空与光照控制系统
1、天空盒材质

(1) 创建 DynamicSkybox 天空盒材质,Sky Tint :#95B7DD,Ground:#6C80A4

2、昼夜光照变化

(1) 给 Directional Light 添加 Assets/Scripts/Environment/SunManager.cs

using UnityEngine;
using System;[RequireComponent(typeof(Light))]
public class SunManager : MonoBehaviour
{[Header("地理位置设置")][Range(0f, 90f)][SerializeField] private float latitude = 39.9f; // 北京实际纬度[Range(-180f, 180f)][SerializeField] private float longitude = 116.4f;[Header("时间管理器")]private TimeManager timeManager;[Header("光照参数")][Range(0f, 2f)][SerializeField] private float sunIntensity = 1.8f;    // 太阳强度(山谷场景适配)[Range(0f, 0.5f)][SerializeField] private float moonIntensity = 0.3f; // 夜晚环境光[Range(0f, 1f)][SerializeField] private float ambientIntensity = 0.3f;[Header("阴影淡出系统")][Range(5f, 30f)][SerializeField] private float shadowFadeStartAltitude = 25f;[Range(0f, 15f)][SerializeField] private float shadowFadeEndAltitude = 8f;[Range(0f, 2f)][SerializeField] private float shadowFadeDuration = 1.2f;[Range(0f, 1f)][SerializeField] private float minShadowStrength = 0.1f;[Header("季节阴影调整")][SerializeField] private bool enableSeasonalShadowAdjustment = true;[Range(-5f, 5f)][SerializeField] private float summerShadowOffset = -2f;[Range(-5f, 5f)][SerializeField] private float winterShadowOffset = 2f;[Header("大气散射")][Range(0f, 0.1f)][SerializeField] private float rayleighScattering = 0.033f;[Range(0f, 0.1f)][SerializeField] private float mieScattering = 0.002f; [Range(0.5f, 5f)][SerializeField] private float atmosphereThickness = 1.0f; [Header("季节调整")][Range(-30f, 0f)][SerializeField] private float winterSunOffset = 5f;   // 冬季纬度北移[Range(-30f, 30f)][SerializeField] private float summerSunOffset = -5f; // 夏季纬度南移[Header("天空盒设置")][SerializeField] private Material proceduralSkybox;[Range(0.01f, 0.2f)][SerializeField] private float sunSize = 0.04f;[Range(1f, 20f)][SerializeField] private float sunConvergence = 5f;[Header("时间阶段设置")][Range(10f, 70f)][SerializeField] private float eveningStartAltitude = 60f; [Range(-10f, 5f)][SerializeField] private float sunsetStartAltitude = 10f; [Range(-10f, 0f)][SerializeField] private float nightStartAltitude = -3f;[Header("环境光调整")][Range(0f, 2f)][SerializeField] private float duskAmbientBoost = 1.3f;[Range(0f, 1f)][SerializeField] private float minAmbientAtDusk = 0.5f;[Header("调试选项")][SerializeField] private bool showDebugInfo = false;#region 配置只读属性(外部仅能读取)public float Latitude => latitude;public float Longitude => longitude;public float SunIntensity => sunIntensity;public float MoonIntensity => moonIntensity;public float AmbientIntensity => ambientIntensity;public float ShadowFadeStartAltitude => shadowFadeStartAltitude;public float ShadowFadeEndAltitude => shadowFadeEndAltitude;public float ShadowFadeDuration => shadowFadeDuration;public float MinShadowStrength => minShadowStrength;public bool EnableSeasonalShadowAdjustment => enableSeasonalShadowAdjustment;public float SummerShadowOffset => summerShadowOffset;public float WinterShadowOffset => winterShadowOffset;public float WinterSunOffset => winterSunOffset;public float SummerSunOffset => summerSunOffset;public float RayleighScattering => rayleighScattering;public float MieScattering => mieScattering;public float AtmosphereThickness => atmosphereThickness;public Material ProceduralSkybox => proceduralSkybox;public float SunSize => sunSize;public float SunConvergence => sunConvergence;public float EveningStartAltitude => eveningStartAltitude;public float SunsetStartAltitude => sunsetStartAltitude;public float NightStartAltitude => nightStartAltitude;public float DuskAmbientBoost => duskAmbientBoost;public float MinAmbientAtDusk => minAmbientAtDusk;public bool ShowDebugInfo => showDebugInfo;#endregionpublic float SolarAltitude { get; private set; }public float SolarAzimuth { get; private set; }public bool IsNight { get; private set; }public bool IsEvening { get; private set; }public bool IsSunset { get; private set; }public bool IsDusk { get; private set; }public float CurrentShadowStrength { get; private set; }public bool ShadowsEnabled { get; private set; }private Light sunLight;private float originalLatitude;private bool wasNight;private bool wasEvening;private bool wasSunset;private bool wasDusk;private float lastSolarAltitude = 0f;private float shadowFadeStartTime = 0f;private bool isShadowFading = false;private const float axialTilt = 23.44f;void Start(){sunLight = GetComponent<Light>();originalLatitude = latitude;timeManager = TimeManager.Instance;if (proceduralSkybox == null && RenderSettings.skybox != null){proceduralSkybox = RenderSettings.skybox;}RenderSettings.ambientMode = UnityEngine.Rendering.AmbientMode.Trilight;if (TimeManager.Instance != null){UpdateSeasonalEffects(TimeManager.Instance.CurrentDateTime.Month);UpdateSunPosition();}wasNight = IsNight;wasEvening = false;wasSunset = false;wasDusk = false;lastSolarAltitude = SolarAltitude;ShadowsEnabled = true;CurrentShadowStrength = 1f;UpdateSunVisualSize();}private void OnEnable(){if (this == null) return;if (Singleton<EventCenter>.IsApplicationQuitting()) return;var eventCenter = EventCenter.Instance;if (eventCenter != null){eventCenter.Subscribe<float>(EventNameConst.OnTimeOfDayChanged, OnTimeOfDayChanged);eventCenter.Subscribe<bool>(EventNameConst.OnDayNightCycleChanged, OnDayNightCycleChanged);OnTimeOfDayChanged(0f);}else{Debug.LogError("SunManager: EventCenter.Instance 为空,无法订阅事件");}}private void OnDisable(){if (this == null) return;if (Singleton<EventCenter>.IsApplicationQuitting()) return;var eventCenter = EventCenter.Instance;if (eventCenter != null){eventCenter.Unsubscribe<float>(EventNameConst.OnTimeOfDayChanged, OnTimeOfDayChanged);eventCenter.Unsubscribe<bool>(EventNameConst.OnDayNightCycleChanged, OnDayNightCycleChanged);}}void OnDestroy(){}#region EventCenter 事件处理private void OnTimeOfDayChanged(float currentTime){UpdateSunPosition();UpdateLighting();if (TimeManager.Instance != null && showDebugInfo){var currentDateTime = TimeManager.Instance.CurrentDateTime;if (currentDateTime.Hour == 12 && currentDateTime.Minute == 0){PredictSunriseSunsetTimes(currentDateTime.DayOfYear);LogSeasonalShadowInfo();}}}private void OnDayNightCycleChanged(bool isDay){if (showDebugInfo){Debug.Log($"昼夜切换: {(isDay ? "白天" : "黑夜")}");}if (!isDay && ShadowsEnabled){CheckShadowFadeStart();}}#endregionvoid Update(){CheckTimePhaseTransitions();UpdateShadowFade();}#region 时间管理器事件处理private void OnHourChanged(DateTime dateTime){if (showDebugInfo && dateTime.Hour == 12){PredictSunriseSunsetTimes(dateTime.DayOfYear);LogSeasonalShadowInfo();}}private void OnDayChanged(DateTime dateTime){UpdateSeasonalEffects(dateTime.Month);if (showDebugInfo){PredictSunriseSunsetTimes(dateTime.DayOfYear);}}private void OnSunPositionUpdate(float sunHeight){UpdateSunPosition();UpdateLighting();}#endregion#region 太阳位置计算private void UpdateSunPosition(){if (timeManager == null) return;float currentHour = timeManager.GetTotalHours();int currentDay = timeManager.GetDayOfYear();float solarDeclinationRad = CalculateSolarDeclination(currentDay);float solarTime = CalculateSolarTime(currentHour);float hourAngle = (solarTime - 12f) * 15f;float latRad = latitude * Mathf.Deg2Rad;float sinAltitude = Mathf.Sin(latRad) * Mathf.Sin(solarDeclinationRad) +Mathf.Cos(latRad) * Mathf.Cos(solarDeclinationRad) * Mathf.Cos(hourAngle * Mathf.Deg2Rad);sinAltitude = Mathf.Clamp(sinAltitude, -1f, 1f);SolarAltitude = Mathf.Asin(sinAltitude) * Mathf.Rad2Deg;float cosAzimuth = (Mathf.Sin(solarDeclinationRad) * Mathf.Cos(latRad) -Mathf.Cos(solarDeclinationRad) * Mathf.Sin(latRad) * Mathf.Cos(hourAngle * Mathf.Deg2Rad)) /Mathf.Cos(SolarAltitude * Mathf.Deg2Rad);cosAzimuth = Mathf.Clamp(cosAzimuth, -1f, 1f);SolarAzimuth = Mathf.Acos(cosAzimuth) * Mathf.Rad2Deg;if (hourAngle > 0) SolarAzimuth = 360f - SolarAzimuth;transform.rotation = Quaternion.Euler(SolarAltitude, SolarAzimuth, 0);IsNight = SolarAltitude < nightStartAltitude;CheckShadowFadeStart();// 关键调试日志:实时输出太阳高度(确认是否正常)if (showDebugInfo && (timeManager.CurrentDateTime.Hour >= 14 && timeManager.CurrentDateTime.Hour <= 16)){Debug.Log($"时间={timeManager.CurrentDateTime:HH:mm}, 纬度={latitude:F1}°, " +$"太阳高度={SolarAltitude:F1}°, 光照强度={sunLight.intensity:F2}");}}private float CalculateSolarDeclination(int dayOfYear){float dayAngle = 2f * Mathf.PI * (dayOfYear - 1) / 365f;return axialTilt * Mathf.Sin(2f * Mathf.PI * (284f + dayOfYear) / 365f) * Mathf.Deg2Rad;}private float CalculateSolarTime(float currentHour){float standardTime = currentHour;float timeZoneCorrection = (longitude / 15f) - 8f;standardTime -= timeZoneCorrection;int dayOfYear = timeManager.CurrentDateTime.DayOfYear;float B = (dayOfYear - 81) * 2f * Mathf.PI / 365f;float equationOfTime = 9.87f * Mathf.Sin(2f * B) - 7.53f * Mathf.Cos(B) - 1.5f * Mathf.Sin(B);return standardTime + equationOfTime / 60f;}private void PredictSunriseSunsetTimes(int dayOfYear){float solarDeclinationRad = CalculateSolarDeclination(dayOfYear);float latRad = latitude * Mathf.Deg2Rad;float cosHourAngle = -Mathf.Tan(latRad) * Mathf.Tan(solarDeclinationRad) - 0.0145f;cosHourAngle = Mathf.Clamp(cosHourAngle, -1f, 1f);if (Mathf.Abs(cosHourAngle) <= 1f){float hourAngle = Mathf.Acos(cosHourAngle) * Mathf.Rad2Deg;float sunriseSolarTime = 12f - hourAngle / 15f;float sunsetSolarTime = 12f + hourAngle / 15f;float sunriseLocal = ConvertSolarToLocalTime(sunriseSolarTime, dayOfYear);float sunsetLocal = ConvertSolarToLocalTime(sunsetSolarTime, dayOfYear);Debug.Log($"预测日出时间: {FormatTime(sunriseLocal)}, 日落时间: {FormatTime(sunsetLocal)}");}}private string FormatTime(float time){int hour = Mathf.FloorToInt(time);int minute = Mathf.FloorToInt((time - hour) * 60f);return $"{hour:00}:{minute:00}";}private float ConvertSolarToLocalTime(float solarTime, int dayOfYear){float timeZoneCorrection = (longitude / 15f) - 8f;float B = (dayOfYear - 81) * 2f * Mathf.PI / 365f;float equationOfTime = 9.87f * Mathf.Sin(2f * B) - 7.53f * Mathf.Cos(B) - 1.5f * Mathf.Sin(B);return solarTime - equationOfTime / 60f + timeZoneCorrection;}#endregion#region 阴影淡出系统private void CheckShadowFadeStart(){if (timeManager == null) return;float adjustedFadeStartAltitude = GetSeasonallyAdjustedShadowAltitude(shadowFadeStartAltitude);if (SolarAltitude <= adjustedFadeStartAltitude && !isShadowFading && ShadowsEnabled){StartShadowFade();}if (SolarAltitude > adjustedFadeStartAltitude && isShadowFading){ResetShadowFade();}}private void StartShadowFade(){isShadowFading = true;shadowFadeStartTime = timeManager.GetTotalHours();if (showDebugInfo){Debug.Log($"开始阴影淡出: 时间={timeManager.CurrentDateTime:HH:mm}, " +$"太阳高度={SolarAltitude:F1}°, 预计持续时间={shadowFadeDuration}小时");}}private void ResetShadowFade(){isShadowFading = false;CurrentShadowStrength = 1f;ShadowsEnabled = true;if (sunLight != null){sunLight.shadows = LightShadows.Soft;sunLight.shadowStrength = CurrentShadowStrength;}if (showDebugInfo){Debug.Log($"重置阴影: 太阳重新升高到{shadowFadeStartAltitude:F1}°以上");}}private void UpdateShadowFade(){if (!isShadowFading || timeManager == null) return;float adjustedFadeStartAltitude = GetSeasonallyAdjustedShadowAltitude(shadowFadeStartAltitude);float adjustedFadeEndAltitude = GetSeasonallyAdjustedShadowAltitude(shadowFadeEndAltitude);float timeProgress = CalculateShadowFadeTimeProgress();float altitudeProgress = CalculateShadowFadeAltitudeProgress(adjustedFadeStartAltitude, adjustedFadeEndAltitude);float fadeProgress = Mathf.Max(timeProgress, altitudeProgress);CurrentShadowStrength = Mathf.Lerp(1f, minShadowStrength, fadeProgress);if (sunLight != null){sunLight.shadowStrength = CurrentShadowStrength;if (CurrentShadowStrength <= minShadowStrength + 0.05f){sunLight.shadows = LightShadows.None;ShadowsEnabled = false;}else{sunLight.shadows = LightShadows.Soft;ShadowsEnabled = true;}}if (fadeProgress >= 1f){isShadowFading = false;if (showDebugInfo) Debug.Log("阴影淡出完成");}if (showDebugInfo && timeManager.CurrentDateTime.Minute % 15 == 0 && isShadowFading){Debug.Log($"阴影淡出: 进度={fadeProgress:F2}, 强度={CurrentShadowStrength:F2}, " +$"时间进度={timeProgress:F2}, 高度进度={altitudeProgress:F2}");}}private float CalculateShadowFadeTimeProgress(){float elapsedHours = timeManager.GetTotalHours() - shadowFadeStartTime;return Mathf.Clamp01(elapsedHours / shadowFadeDuration);}private float CalculateShadowFadeAltitudeProgress(float startAlt, float endAlt){if (SolarAltitude <= endAlt) return 1f;if (SolarAltitude >= startAlt) return 0f;return Mathf.Clamp01(1f - (SolarAltitude - endAlt) / (startAlt - endAlt));}private float GetSeasonallyAdjustedShadowAltitude(float baseAltitude){if (!enableSeasonalShadowAdjustment || timeManager == null)return baseAltitude;int month = timeManager.CurrentDateTime.Month;float seasonalOffset = 0f;if (month >= 6 && month <= 8){seasonalOffset = summerShadowOffset;}else if (month == 12 || month <= 2){seasonalOffset = winterShadowOffset;}else if (month == 3 || month == 4 || month == 9 || month == 10){if (month == 3 || month == 4)seasonalOffset = Mathf.Lerp(winterShadowOffset, 0f, (month == 3 ? 0.5f : 1f));elseseasonalOffset = Mathf.Lerp(0f, winterShadowOffset, (month == 9 ? 0.5f : 1f));}return baseAltitude + seasonalOffset;}#endregion#region 时间阶段管理private void CheckTimePhaseTransitions(){IsEvening = false;IsSunset = false;IsDusk = false;if (SolarAltitude < eveningStartAltitude && SolarAltitude > sunsetStartAltitude){IsEvening = true;}else if (SolarAltitude <= sunsetStartAltitude && SolarAltitude > nightStartAltitude){IsSunset = true;}else if (SolarAltitude <= nightStartAltitude && SolarAltitude > -12f){IsDusk = true;}if (IsEvening && !wasEvening){OnEveningStart();}else if (IsSunset && !wasSunset){OnSunsetStart();}else if (IsDusk && !wasDusk){OnDuskStart();}else if (IsNight && !wasNight){OnNightStart();}wasEvening = IsEvening;wasSunset = IsSunset;wasDusk = IsDusk;wasNight = IsNight;lastSolarAltitude = SolarAltitude;}private void OnEveningStart(){if (showDebugInfo) Debug.Log($"傍晚开始: {timeManager.CurrentDateTime:HH:mm}, 太阳高度: {SolarAltitude:F1}°");}private void OnSunsetStart(){if (showDebugInfo) Debug.Log($"日落开始: {timeManager.CurrentDateTime:HH:mm}, 太阳高度: {SolarAltitude:F1}°");}private void OnDuskStart(){if (showDebugInfo) Debug.Log($"黄昏开始: {timeManager.CurrentDateTime:HH:mm}, 太阳高度: {SolarAltitude:F1}°");}private void OnNightStart(){if (showDebugInfo) Debug.Log($"夜晚开始: {timeManager.CurrentDateTime:HH:mm}, 太阳高度: {SolarAltitude:F1}°");}#endregion#region 光照与大气效果private void UpdateLighting(){if (sunLight == null) return;float intensity = CalculateSimpleIntensity();Color lightColor = CalculateSimpleColor();float ambient = CalculateSimpleAmbient();sunLight.intensity = intensity;sunLight.color = lightColor;RenderSettings.ambientIntensity = ambient;UpdateSunVisualSize();UpdateAtmosphericScattering();}private float CalculateSimpleIntensity(){if (IsNight){return 0f;}float baseIntensity = sunIntensity * Mathf.Clamp01(SolarAltitude / 90f * 1.2f);if (IsEvening || IsSunset){float duskFactor = Mathf.Clamp01((SolarAltitude - nightStartAltitude) / (eveningStartAltitude - nightStartAltitude));baseIntensity = Mathf.Max(baseIntensity, sunIntensity * 0.4f * duskFactor);}return baseIntensity;}private Color CalculateSimpleColor(){if (IsNight){return new Color(0.7f, 0.8f, 1f);}Color baseColor = new Color(1f, 0.95f, 0.85f);if (IsEvening){float eveningFactor = Mathf.Clamp01((SolarAltitude - sunsetStartAltitude) / (eveningStartAltitude - sunsetStartAltitude));baseColor = Color.Lerp(new Color(1f, 0.9f, 0.8f), baseColor, eveningFactor);}else if (IsSunset){float sunsetFactor = Mathf.Clamp01((SolarAltitude - nightStartAltitude) / (sunsetStartAltitude - nightStartAltitude));baseColor = Color.Lerp(new Color(1f, 0.85f, 0.75f), new Color(1f, 0.9f, 0.8f), sunsetFactor);}else if (IsDusk){float duskFactor = Mathf.Clamp01((SolarAltitude + 12f) / (nightStartAltitude + 12f));baseColor = Color.Lerp(new Color(0.7f, 0.8f, 1f), new Color(1f, 0.85f, 0.75f), duskFactor);}return baseColor;}private float CalculateSimpleAmbient(){if (IsNight){return moonIntensity; }float baseAmbient = ambientIntensity;float altitudeFactor = Mathf.Clamp01((SolarAltitude + 6f) / 30f);baseAmbient *= altitudeFactor;if (IsEvening || IsSunset || IsDusk){baseAmbient = Mathf.Max(baseAmbient, minAmbientAtDusk);if (IsDusk){baseAmbient *= duskAmbientBoost;}}return baseAmbient;}private void UpdateSunVisualSize(){if (proceduralSkybox == null) return;float visualSize = sunSize;if (SolarAltitude > -5f && SolarAltitude < 10f){float sizeFactor = 1.2f - Mathf.Abs(SolarAltitude - 2.5f) / 7.5f;visualSize = sunSize * Mathf.Clamp(sizeFactor, 1f, 1.5f);}else if (SolarAltitude < -5f){visualSize = 0f;}proceduralSkybox.SetFloat("_SunSize", visualSize);proceduralSkybox.SetFloat("_SunSizeConvergence", sunConvergence);}private void UpdateAtmosphericScattering(){float scatteringIntensity = Mathf.Clamp01(1f - Mathf.Abs(SolarAltitude) / 90f);float rayleighFactor = rayleighScattering * scatteringIntensity;Color skyColor = new Color(0.5f * rayleighFactor, 0.7f * rayleighFactor, 1f * rayleighFactor);float mieFactor = mieScattering * scatteringIntensity;Color horizonColor = skyColor;if (IsEvening){float eveningFactor = Mathf.Clamp01((SolarAltitude - sunsetStartAltitude) / (eveningStartAltitude - sunsetStartAltitude));horizonColor = Color.Lerp(new Color(0.8f, 0.7f, 0.6f), skyColor, eveningFactor);}else if (IsSunset){float sunsetFactor = Mathf.Clamp01((SolarAltitude - nightStartAltitude) / (sunsetStartAltitude - nightStartAltitude));horizonColor = Color.Lerp(new Color(0.7f, 0.6f, 0.5f), new Color(0.8f, 0.7f, 0.6f), sunsetFactor);}else if (IsDusk){float duskFactor = Mathf.Clamp01((SolarAltitude + 12f) / (nightStartAltitude + 12f));horizonColor = Color.Lerp(skyColor, new Color(0.7f, 0.6f, 0.5f), duskFactor);}// 白天/黄昏设置if (SolarAltitude > 0 || IsEvening || IsSunset || IsDusk){RenderSettings.ambientSkyColor = skyColor;RenderSettings.ambientEquatorColor = Color.Lerp(skyColor, horizonColor, 0.5f);RenderSettings.ambientGroundColor = new Color(0.2f, 0.2f, 0.2f); RenderSettings.fog = true;RenderSettings.fogColor = horizonColor;RenderSettings.fogDensity = mieFactor * atmosphereThickness * 0.0005f;}else // 夜晚设置{RenderSettings.fog = false; // 关闭全局雾Color nightSkyColor = new Color(0.08f, 0.15f, 0.3f); // 深蓝色(可调整RGB微调深浅)// 环境光仅作用于天空盒,地面环境光去蓝色RenderSettings.ambientSkyColor = nightSkyColor;RenderSettings.ambientEquatorColor = nightSkyColor * 0.8f;RenderSettings.ambientGroundColor = new Color(0.1f, 0.1f, 0.15f); // 地面弱光// 程序化天空盒单独设置(不影响地形)if (proceduralSkybox != null){proceduralSkybox.SetColor("_SkyColor", nightSkyColor);proceduralSkybox.SetFloat("_SunSize", 0f);proceduralSkybox.SetColor("_HorizonColor", nightSkyColor * 0.7f);proceduralSkybox.SetFloat("_FogDensity", 0.0001f); // 仅天空盒内部弱雾}}}#endregion#region 季节与辅助功能private void UpdateSeasonalEffects(int month){latitude = originalLatitude;if (month >= 11 || month <= 2){latitude += winterSunOffset; // 冬季:纬度+5f(北移)}else if (month >= 5 && month <= 8){latitude += summerSunOffset; // 夏季:纬度-5f(南移)}if (showDebugInfo){Debug.Log($"季节更新: 月份={month}, 调整后纬度={latitude:F1}°");}}private void LogSeasonalShadowInfo(){if (!enableSeasonalShadowAdjustment || timeManager == null) return;float adjustedStart = GetSeasonallyAdjustedShadowAltitude(shadowFadeStartAltitude);float adjustedEnd = GetSeasonallyAdjustedShadowAltitude(shadowFadeEndAltitude);Debug.Log($"季节阴影调整: 淡出起始={adjustedStart:F1}°, 淡出结束={adjustedEnd:F1}°, " +$"月份={timeManager.CurrentDateTime.Month}");}public float SeasonFactor{get{if (timeManager == null) return 0.5f;int month = timeManager.CurrentDateTime.Month;return Mathf.Abs(month - 6) / 6f;}}public float NightFactor{get => Mathf.Clamp01((-SolarAltitude - 6f) / 18f);}public void SetTimeForDebugging(int hour, int minute = 0){if (timeManager == null) return;TimeManager.Instance.SetTime(hour, minute);}public string GetLightingStateInfo(){return $"太阳高度: {SolarAltitude:F1}°, 强度: {sunLight.intensity:F2}, " +$"阴影: {(ShadowsEnabled ? "启用" : "禁用")}, 阴影强度: {CurrentShadowStrength:F2}";}public void StartManualShadowFade(){StartShadowFade();}public void ResetManualShadowFade(){ResetShadowFade();}#endregionprivate void OnDrawGizmosSelected(){if (!Application.isPlaying || timeManager == null) return;if (ShadowsEnabled){Gizmos.color = Color.Lerp(Color.red, Color.green, CurrentShadowStrength);}else{Gizmos.color = Color.gray;}Gizmos.DrawSphere(transform.position + transform.forward * 10f, 0.5f);float adjustedStart = GetSeasonallyAdjustedShadowAltitude(shadowFadeStartAltitude);float adjustedEnd = GetSeasonallyAdjustedShadowAltitude(shadowFadeEndAltitude);Gizmos.color = Color.yellow;Vector3 startPosition = transform.position + Vector3.up * adjustedStart;Gizmos.DrawWireCube(startPosition, new Vector3(3f, 0.1f, 3f));Gizmos.color = Color.red;Vector3 endPosition = transform.position + Vector3.up * adjustedEnd;Gizmos.DrawWireCube(endPosition, new Vector3(3f, 0.1f, 3f));Gizmos.color = Color.white;Gizmos.DrawLine(startPosition, endPosition);}
}

(2) 将 DynamicSkybox 材质拖到天空盒设置: Procedural Skybox 字段

(3) 设置 Render Settings

  • Window > Rendering > Lighting Settings
  • 将 DynamicSkybox 拖到 Environment 的 Skybox Material
七、夜间视觉管理
1、可见性控制脚本

(1) 基类 :  Assets/Scripts/Environment/BaseParticleController.cs

using UnityEngine;[RequireComponent(typeof(ParticleSystem))]
public abstract class BaseParticleController : MonoBehaviour
{[Header("可见性设置")]public bool visibleAtNight = true;public bool visibleAtDay = false;[Header("过渡时间设置(分钟)")][Tooltip("日出前多少分钟开始消失")]public float disappearBeforeSunrise = 30f;[Tooltip("日落后多少分钟开始出现")]public float appearAfterSunset = 30f;protected ParticleSystem particleSys;protected ParticleSystemRenderer particleRenderer;protected bool isDayTime;protected Vector3 originalPosition;protected float currentSunHeight;protected bool isInTransition;protected virtual void Awake(){particleSys = GetComponent<ParticleSystem>();particleRenderer = GetComponent<ParticleSystemRenderer>();originalPosition = transform.position;SetParticleState(false);}protected virtual void OnEnable(){// 自身实例检查(避免组件已销毁但仍执行)if (this == null) return;// 退出阶段跳过订阅(避免应用退出时获取实例)if (Singleton<TimeManager>.IsApplicationQuitting()) return;if (TimeManager.Instance != null){isDayTime = TimeManager.Instance.IsDayTime;// 订阅事件(绑定)TimeManager.Instance.OnDayNightChange += HandleDayNightChange;TimeManager.Instance.OnSunPositionUpdate += HandleSunPositionUpdate;TimeManager.Instance.OnTimeProgressUpdate += HandleTimeProgressUpdate;// 初始化状态HandleDayNightChange(isDayTime);}else{Debug.LogWarning($"[{gameObject.name}] 未找到 TimeManager 实例!粒子无法响应昼夜变化", this);}}protected virtual void OnDisable(){// 自身实例检查if (this == null) return;// 退出阶段跳过解绑(避免应用退出时获取实例)if (Singleton<TimeManager>.IsApplicationQuitting()) return;if (TimeManager.Instance != null){// 解绑事件(必须和订阅一一对应,避免内存泄漏)TimeManager.Instance.OnDayNightChange -= HandleDayNightChange;TimeManager.Instance.OnSunPositionUpdate -= HandleSunPositionUpdate;TimeManager.Instance.OnTimeProgressUpdate -= HandleTimeProgressUpdate;}}protected virtual void OnDestroy(){// 优先判断是否在退出中,再判断实例是否存在(避免获取实例失败警告)if (Singleton<TimeManager>.IsApplicationQuitting()) return;if (this == null) return;// 双重保险:再次解绑(防止OnDisable因异常未执行)if (TimeManager.Instance != null){TimeManager.Instance.OnDayNightChange -= HandleDayNightChange;TimeManager.Instance.OnSunPositionUpdate -= HandleSunPositionUpdate;TimeManager.Instance.OnTimeProgressUpdate -= HandleTimeProgressUpdate;}}protected virtual void HandleDayNightChange(bool isDayTime){this.isDayTime = isDayTime;UpdateParticleStateBasedOnTime();}protected virtual void HandleSunPositionUpdate(float sunHeight){currentSunHeight = sunHeight;UpdateParticleStateBasedOnTime();}protected virtual void HandleTimeProgressUpdate(float progress){// 子类可以重写这个方法来实现基于时间进度的效果}protected virtual void UpdateParticleStateBasedOnTime(){if (TimeManager.Instance == null) return;float currentHour = TimeManager.Instance.GetTotalHours();float sunriseHour = TimeManager.Instance.SunriseHour;float sunsetHour = TimeManager.Instance.SunsetHour;// 转换为小时float disappearBeforeSunriseHours = disappearBeforeSunrise / 60f;float appearAfterSunsetHours = appearAfterSunset / 60f;bool shouldBeActive = CalculateShouldBeActive(currentHour, sunriseHour, sunsetHour,disappearBeforeSunriseHours, appearAfterSunsetHours);SetParticleState(shouldBeActive);}protected virtual bool CalculateShouldBeActive(float currentHour, float sunriseHour,float sunsetHour, float disappearBeforeSunriseHours, float appearAfterSunsetHours){// 处理跨天的情况if (sunriseHour > sunsetHour){// 日出在第二天if (currentHour >= sunriseHour - disappearBeforeSunriseHours ||currentHour <= sunsetHour + appearAfterSunsetHours){return visibleAtNight;}else if (currentHour > sunsetHour + appearAfterSunsetHours &&currentHour < sunriseHour - disappearBeforeSunriseHours){return visibleAtDay;}}else{// 正常情况if (currentHour >= sunsetHour + appearAfterSunsetHours ||currentHour <= sunriseHour - disappearBeforeSunriseHours){return visibleAtNight;}else if (currentHour > sunriseHour - disappearBeforeSunriseHours &&currentHour < sunsetHour + appearAfterSunsetHours){return visibleAtDay;}}return false;}protected virtual void SetParticleState(bool active){if (active && !particleSys.isPlaying){particleSys.Play();}else if (!active && particleSys.isPlaying){particleSys.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);}if (particleRenderer != null){particleRenderer.enabled = active;}}[ContextMenu("调试:切换粒子状态")]public void ToggleParticles(){SetParticleState(!particleSys.isPlaying);}[ContextMenu("调试:刷新粒子状态")]public void RefreshState(){if (TimeManager.Instance != null){HandleDayNightChange(TimeManager.Instance.IsDayTime);}}
}

(2) 视野外不渲染

using UnityEngine;public class ParticleCulling : MonoBehaviour
{[Header("视野剔除设置")][Tooltip("检测间隔(秒)")][SerializeField, Range(0.01f, 0.5f)] private float checkInterval = 0.05f; [Tooltip("边界比例")][SerializeField] private float boundsExpandRatio = 1.2f; // 关键2:扩大检测边界,提前显示[Tooltip("透明度渐变时间(秒)")][SerializeField] private float fadeTime = 0.3f; // 关键3:添加渐变过渡[Tooltip("手动设置粒子边界(基于粒子Shape模块)")]public Bounds manualParticleBounds;private Camera mainCamera;private ParticleSystem particleSys;private ParticleSystemRenderer particleRenderer;private bool lastInViewState;private float lastCheckTime;private Transform cameraTransform;private float currentAlpha; // 渐变用透明度private ParticleSystem.MainModule mainModule; // 粒子主模块(控制透明度)private void Start(){InitializeComponents();// 初始化透明度(默认隐藏)currentAlpha = 0f;UpdateParticleAlpha();}private void InitializeComponents(){particleSys = GetComponent<ParticleSystem>();particleRenderer = GetComponent<ParticleSystemRenderer>();mainModule = particleSys.main; // 获取主模块(控制颜色/透明度)if (particleSys == null){Debug.LogError("ParticleCulling: 未找到 ParticleSystem 组件!", this);enabled = false;return;}if (particleRenderer == null){Debug.LogError("ParticleCulling: 未找到 ParticleSystemRenderer 组件!", this);enabled = false;return;}mainCamera = Camera.main;cameraTransform = mainCamera?.transform;lastInViewState = IsInCameraFrustum();// 初始不暂停粒子,只隐藏透明度(避免启动延迟)particleSys.Play();particleRenderer.enabled = true;}private void Update(){if (Time.time - lastCheckTime >= checkInterval){PerformCullingCheck();lastCheckTime = Time.time;}// 实时更新透明度渐变if (lastInViewState && currentAlpha < 1f){currentAlpha = Mathf.MoveTowards(currentAlpha, 1f, Time.deltaTime / fadeTime);UpdateParticleAlpha();}else if (!lastInViewState && currentAlpha > 0f){currentAlpha = Mathf.MoveTowards(currentAlpha, 0f, Time.deltaTime / fadeTime);UpdateParticleAlpha();}}private void PerformCullingCheck(){if (!EnsureCameraReference()) return;bool isInView = IsInCameraFrustum();if (isInView != lastInViewState){lastInViewState = isInView;}}private bool IsInCameraFrustum(){if (mainCamera == null || particleRenderer == null)return false;// 扩大手动边界(提前检测到视野内,预留渐变时间)Bounds bounds = manualParticleBounds.size != Vector3.zero ? manualParticleBounds : particleRenderer.bounds;bounds.Expand(boundsExpandRatio); // 按比例扩大边界if (bounds.size == Vector3.zero){Vector3 viewportPoint = mainCamera.WorldToViewportPoint(transform.position);return viewportPoint.x >= -0.2 && viewportPoint.x <= 1.2 && // 扩大视口检测范围viewportPoint.y >= -0.2 && viewportPoint.y <= 1.2 &&viewportPoint.z > 0;}Plane[] frustumPlanes = GeometryUtility.CalculateFrustumPlanes(mainCamera);return GeometryUtility.TestPlanesAABB(frustumPlanes, bounds);}// 控制粒子透明度(渐变核心)private void UpdateParticleAlpha(){// 保留粒子原有颜色,只修改透明度Color originalColor = mainModule.startColor.color;originalColor.a = currentAlpha;mainModule.startColor = originalColor;// 透明度为0时,禁用渲染(优化性能)particleRenderer.enabled = currentAlpha > 0.01f;}#region 调试工具(保留)[ContextMenu("刷新视野检测")]public void RefreshCulling(){if (!EnsureCameraReference()) return;bool isInView = IsInCameraFrustum();lastInViewState = isInView;currentAlpha = isInView ? 1f : 0f;UpdateParticleAlpha();}private void OnDrawGizmosSelected(){if (!Application.isPlaying) return;if (particleRenderer != null){Gizmos.color = lastInViewState ? Color.green : Color.red;Bounds bounds = manualParticleBounds.size != Vector3.zero ? manualParticleBounds : particleRenderer.bounds;bounds.Expand(boundsExpandRatio); // 绘制扩大后的边界Gizmos.DrawWireCube(bounds.center, bounds.size);#if UNITY_EDITORUnityEditor.Handles.Label(transform.position, $"视野内: {lastInViewState}\n透明度: {currentAlpha:F2}");
#endif}}#endregion// 其他方法(EnsureCameraReference)private bool EnsureCameraReference(){if (mainCamera != null && cameraTransform != null) return true;mainCamera = Camera.main;cameraTransform = mainCamera?.transform;return mainCamera != null;}
}
 2、萤火虫

(1) 萤火虫预制体

① 资产来源:"D:\Unity\Using\Fireflies.unitypackage" 

② Shader更改

Shader "Universal Render Pipeline/Custom/Firefly" {Properties {_Color ("Color", Color) = (1,1,1,1)[HDR]_EmissionColor ("Emission Color", Color) = (1,1,1,1)_MainTex ("Albedo (RGB)", 2D) = "white" {}_Emission ("Emission Map", 2D) = "white" {}_Normal ("Normal Map", 2D) = "bump" {}_Glossiness ("Smoothness", Range(0,1)) = 0.5_Specular ("Specular", Range(0,1)) = 0.0_Wings ("Wings (RGBA)", 2D) = "white" {}_WingGloss ("Wing Smoothness", Range(0,1)) = 0.5_WingSpecular ("Wing Specular", Range(0,1)) = 0.0}SubShader {Tags {"Queue"="Transparent" "RenderType"="Transparent""RenderPipeline"="UniversalPipeline"}LOD 200Pass {Name "ForwardLit"Tags { "LightMode"="UniversalForward" }Blend SrcAlpha OneMinusSrcAlphaZWrite OffCull OffHLSLPROGRAM#pragma vertex vert#pragma fragment frag#pragma multi_compile _ _MAIN_LIGHT_SHADOWS#pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE#pragma multi_compile_fog#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"struct Attributes {float4 positionOS : POSITION;float2 uv : TEXCOORD0;float3 normalOS : NORMAL;float4 tangentOS : TANGENT;float4 color : COLOR;};struct Varyings {float4 positionHCS : SV_POSITION;float2 uv : TEXCOORD0;float3 normalWS : TEXCOORD1;float4 color : COLOR;float fogFactor : TEXCOORD2;};TEXTURE2D(_MainTex);    SAMPLER(sampler_MainTex);TEXTURE2D(_Emission);   SAMPLER(sampler_Emission);TEXTURE2D(_Normal);     SAMPLER(sampler_Normal);TEXTURE2D(_Wings);      SAMPLER(sampler_Wings);CBUFFER_START(UnityPerMaterial)float4 _Color;float4 _EmissionColor;float _Glossiness;float _Specular;float _WingGloss;float _WingSpecular;CBUFFER_ENDVaryings vert(Attributes IN) {Varyings OUT;OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);OUT.uv = IN.uv;OUT.normalWS = TransformObjectToWorldNormal(IN.normalOS);OUT.color = IN.color;OUT.fogFactor = ComputeFogFactor(OUT.positionHCS.z);return OUT;}half4 frag(Varyings IN) : SV_Target {// 采样基础纹理half4 albedo = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, IN.uv) * _Color;half3 emission = SAMPLE_TEXTURE2D(_Emission, sampler_Emission, IN.uv).rgb * _EmissionColor.rgb;half3 normalTS = UnpackNormal(SAMPLE_TEXTURE2D(_Normal, sampler_Normal, IN.uv));// URP光照计算Light mainLight = GetMainLight();float3 lightDir = mainLight.direction;float NdotL = saturate(dot(normalize(IN.normalWS), lightDir));float3 diffuse = mainLight.color * NdotL;// 混合输出half4 finalColor;finalColor.rgb = albedo.rgb * diffuse + emission;finalColor.a = albedo.a * (1 - step(0.5, IN.color.r)); // 透明通道处理// 应用雾效finalColor.rgb = MixFog(finalColor.rgb, IN.fogFactor);return finalColor;}ENDHLSL}}FallBack "Universal Render Pipeline/Simple Lit"
}

(2) 添加到场景

① 以Player为父物体,添加 Fireflies 预制体,Position.y改为1.5

② 给场景中的的 Fireflies 添加 Assets/Scripts/Environment/FireflyParticleController.cs

using UnityEngine;public class FireflyParticleController : BaseParticleController
{[Header("萤火虫行为设置")]public float maxDistanceFromPlayer = 20f;public float spawnRadius = 5f;[Tooltip("位置更新间隔(秒)")]public float positionUpdateInterval = 5f; // 增加间隔减少性能开销[Tooltip("最大萤火虫数量限制")]public int maxFireflyCount = 30;private Transform playerTransform;private float lastUpdateTime;private ParticleSystem.EmissionModule emissionModule;private float originalEmissionRate;protected override void Awake(){base.Awake();if (particleSys == null){Debug.LogWarning($"[{gameObject.name}] 未找到ParticleSystem组件!", this);return;}emissionModule = particleSys.emission;originalEmissionRate = emissionModule.rateOverTime.constant;AdjustEmissionRate();}private void Start(){if (Singleton<TimeManager>.IsApplicationQuitting()) return;GameObject player = GameObject.FindGameObjectWithTag("Player");if (player != null){playerTransform = player.transform;}else{Debug.LogWarning($"[{gameObject.name}] 未找到玩家对象!萤火虫将无法跟随玩家", this);}}private void Update(){if (Singleton<TimeManager>.IsApplicationQuitting() || !particleSys.isPlaying || playerTransform == null){return;}if (Time.time - lastUpdateTime >= positionUpdateInterval){UpdateFireflyPosition();lastUpdateTime = Time.time;}}protected override void SetParticleState(bool active){if (this == null || Singleton<TimeManager>.IsApplicationQuitting()) return;if (active && playerTransform != null){if (!particleSys.isPlaying){MoveToPlayerArea();particleSys.Play();}}else{if (particleSys.isPlaying){particleSys.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);}}if (particleRenderer != null){particleRenderer.enabled = active;}}protected override bool CalculateShouldBeActive(float currentHour, float sunriseHour,float sunsetHour, float disappearBeforeSunriseHours, float appearAfterSunsetHours){// 萤火虫只在夜晚显示,遵循基类规则return base.CalculateShouldBeActive(currentHour, sunriseHour, sunsetHour,disappearBeforeSunriseHours, appearAfterSunsetHours);}private void UpdateFireflyPosition(){if (playerTransform == null) return;float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position);if (distanceToPlayer > maxDistanceFromPlayer){MoveToPlayerArea();}}private void MoveToPlayerArea(){if (playerTransform == null || Singleton<TimeManager>.IsApplicationQuitting()) return;Vector3 randomOffset = new Vector3(Random.Range(-spawnRadius, spawnRadius),Random.Range(0, spawnRadius * 0.5f),Random.Range(-spawnRadius, spawnRadius));transform.position = playerTransform.position + randomOffset;}private void AdjustEmissionRate(){var mainModule = particleSys.main;float particleLifetime = mainModule.startLifetime.constant;float adjustedRate = maxFireflyCount / Mathf.Max(particleLifetime, 1f);emissionModule.rateOverTime = Mathf.Min(adjustedRate, originalEmissionRate);}[ContextMenu("重新定位萤火虫")]public void RepositionFireflies(){if (Singleton<TimeManager>.IsApplicationQuitting()) return;MoveToPlayerArea();}[ContextMenu("重置发射率")]public void ResetEmissionRate(){emissionModule.rateOverTime = originalEmissionRate;}private void OnValidate(){if (!Application.isPlaying || Singleton<TimeManager>.IsApplicationQuitting() || particleSys == null){return;}AdjustEmissionRate();}protected override void OnDestroy(){base.OnDestroy();// 子类清理:释放引用,双重保险停止粒子playerTransform = null;if (particleSys != null && particleSys.isPlaying){particleSys.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);}}
}

 3、云层控制

(1) 给三个云层的子物体粒子云添加 ParticleCulling.cs组件

(2) 配置参数

云层名称manualParticleBounds 配置配置依据说明
Particle_Cloud_Atmos_ACenter:(1000, 164.1, -492)Extents:(750, 5, 750)1. 父物体位置直接作为中心点(粒子自身 Position 为 (0,0,0));2. 粒子 Shape 是 Box,Scale (1500,10,1500),Extents = Scale/2
Particle_Cloud_Atmos_BCenter:(1409.3, 85.8, -508.4)Extents:(270.65, 2.5, 270.65)1. 父物体位置直接作为中心点;2. 粒子 Shape 是 Circle,Radius=54.13 × Scale=5 = 实际半径 270.65(X/Z 方向);3. Y 方向 Scale=5,Extents=5/2=2.5
第三块云(未命名,按之前配置)Center:(578.4, 85.8, -508.4)Extents:(270.65, 2.5, 270.65)1. 父物体位置直接作为中心点;2. 粒子 Shape 与第二块云一致(Circle + 相同 Scale/Radius),故 Extents 相同

(3) 给 Particle_Cloud_Atmos_A的子物体添加 Scripts/Environment/CloudParticleController.cs

using UnityEngine;public class CloudParticleController : MonoBehaviour
{[Header("云层设置")][Tooltip("显示粒子")]public float startHour = 4f;[Tooltip("隐藏粒子")]public float endHour = 19.5f;[Header("透明度变化时间")][Tooltip("透明度增大开始时间")]public float morningFadeStart = 4f;[Tooltip("透明度增大结束时间")]public float morningFadeEnd = 4.1f;[Tooltip("上午透明度变化开始时间")]public float noonFadeStart = 10f;[Tooltip("上午透明度变化结束时间")]public float noonFadeEnd = 10.1f;[Tooltip("透明度下降开始时间")]public float afternoonFadeStart = 10.1f;[Tooltip("透明度下降结束时间")]public float afternoonFadeEnd = 10.2f;[Header("发射率变化时间")][Tooltip("发射率第一阶段变化开始时间")]public float emissionFade1Start = 9f;[Tooltip("发射率第一阶段变化结束时间")]public float emissionFade1End = 9.4f;[Tooltip("发射率第二阶段变化开始时间")]public float emissionFade2Start = 9.4f;[Tooltip("发射率第二阶段变化结束时间")]public float emissionFade2End = 9.5f;[Header("云层外观设置")][Tooltip("最小透明度")]public float minAlpha = 0.01f;[Tooltip("最大透明度")]public float maxAlpha = 0.4f;[Tooltip("最大发射率")]public float maxEmissionRate = 10f;[Tooltip("中等发射率")]public float midEmissionRate = 0.1f;[Header("调试")]public bool showDebugInfo = false;private ParticleSystem particleSys;private ParticleSystemRenderer particleRenderer;private ParticleSystem.EmissionModule emissionModule;private Color originalParticleColor;private float targetEmissionRate;private bool isActive = false;private float lastUpdateTime = -1f;private void Awake(){particleSys = GetComponent<ParticleSystem>();particleRenderer = GetComponent<ParticleSystemRenderer>();emissionModule = particleSys.emission;targetEmissionRate = emissionModule.rateOverTime.constant;// 保存原始颜色var mainModule = particleSys.main;if (mainModule.startColor.mode == ParticleSystemGradientMode.Color){originalParticleColor = mainModule.startColor.color;}// 初始状态:隐藏SetCloudState(false);}private void Start(){if (Singleton<TimeManager>.IsApplicationQuitting()) return;// 初始更新UpdateCloudState();}private void Update(){if (Time.time - lastUpdateTime >= 0.5f){UpdateCloudState();lastUpdateTime = Time.time;}}private void UpdateCloudState(){if (Singleton<TimeManager>.IsApplicationQuitting()){SetCloudState(false); // 退出时强制隐藏粒子return;}if (TimeManager.Instance == null) return;float currentHour = TimeManager.Instance.GetTotalHours();// 检查是否在显示时间内bool shouldBeActive = currentHour >= startHour && currentHour <= endHour;if (shouldBeActive){UpdateCloudAppearance(currentHour);}else{SetCloudState(false);}if (showDebugInfo && shouldBeActive){var mainModule = particleSys.main;float currentAlpha = mainModule.startColor.mode == ParticleSystemGradientMode.Color ?mainModule.startColor.color.a : 0f;float currentEmission = emissionModule.rateOverTime.constant;Debug.Log($"[Cloud] 时间: {currentHour:F2}, 透明度: {currentAlpha:F2}, " +$"发射率: {currentEmission:F2}, 激活: {isActive}, 粒子数: {particleSys.particleCount}");}}private void UpdateCloudAppearance(float currentHour){// 设置云层激活SetCloudState(true);// 更新透明度float alpha = CalculateAlpha(currentHour);UpdateAlpha(alpha);// 更新发射率float emission = CalculateEmission(currentHour);UpdateEmission(emission);}private float CalculateAlpha(float currentHour){// 早上透明度变化:4点到4.1点,从0到0.4if (currentHour >= morningFadeStart && currentHour <= morningFadeEnd){float progress = (currentHour - morningFadeStart) / (morningFadeEnd - morningFadeStart);return Mathf.Lerp(0f, maxAlpha, progress);}// 透明度变化:10点到10.1点,从0.4到0.01if (currentHour >= noonFadeStart && currentHour <= noonFadeEnd){float progress = (currentHour - noonFadeStart) / (noonFadeEnd - noonFadeStart);return Mathf.Lerp(maxAlpha, minAlpha, progress);}// 透明度变化:10.1点到10.2点,从0.01到0if (currentHour >= afternoonFadeStart && currentHour <= afternoonFadeEnd){float progress = (currentHour - afternoonFadeStart) / (afternoonFadeEnd - afternoonFadeStart);return Mathf.Lerp(minAlpha, 0f, progress);}// 其他时间保持当前阶段的透明度if (currentHour < morningFadeStart)return 0f; // 4点前不显示else if (currentHour <= noonFadeStart)return maxAlpha; // 4.1点到10点保持0.4else if (currentHour <= afternoonFadeStart)return minAlpha; // 10.1点前保持0.01elsereturn 0f; // 10.2点后透明度为0}private float CalculateEmission(float currentHour){// 4点到4.1点:发射率从0增加到10if (currentHour >= morningFadeStart && currentHour <= morningFadeEnd){float progress = (currentHour - morningFadeStart) / (morningFadeEnd - morningFadeStart);return Mathf.Lerp(0f, maxEmissionRate, progress);}// 4.1点到9点:保持最大发射率10if (currentHour > morningFadeEnd && currentHour < emissionFade1Start){return maxEmissionRate;}// 9点到9.4点:发射率从10降低到0.1if (currentHour >= emissionFade1Start && currentHour <= emissionFade1End){float progress = (currentHour - emissionFade1Start) / (emissionFade1End - emissionFade1Start);return Mathf.Lerp(maxEmissionRate, midEmissionRate, progress);}// 9.4点到9.5点:发射率从0.1降低到0if (currentHour >= emissionFade2Start && currentHour <= emissionFade2End){float progress = (currentHour - emissionFade2Start) / (emissionFade2End - emissionFade2Start);return Mathf.Lerp(midEmissionRate, 0f, progress);}// 其他时间if (currentHour < morningFadeStart)return 0f; // 4点前不发射else if (currentHour > emissionFade1End && currentHour < emissionFade2Start)return midEmissionRate; // 9.4点到9.5点保持0.1else if (currentHour > emissionFade2End && currentHour <= endHour)return 0f; // 9.5点到19.5点不发射elsereturn 0f; // 其他时间不发射}private void UpdateAlpha(float alpha){var mainModule = particleSys.main;if (mainModule.startColor.mode == ParticleSystemGradientMode.Color){Color color = originalParticleColor;color.a = alpha;mainModule.startColor = color;}}private void UpdateEmission(float emissionRate){emissionModule.rateOverTime = emissionRate;}private void SetCloudState(bool active){if (this == null) return;if (active != isActive){isActive = active;if (particleRenderer != null){particleRenderer.enabled = active;}if (active && !particleSys.isPlaying){particleSys.Play();}else if (!active && particleSys.isPlaying){particleSys.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);}}}// 退出时强制清理粒子protected virtual void OnDestroy(){// 应用退出时跳过(避免获取实例)if (Singleton<TimeManager>.IsApplicationQuitting()) return;// 强制停止粒子,释放资源SetCloudState(false);}[ContextMenu("调试云层状态")]public void DebugCloudState(){if (Singleton<TimeManager>.IsApplicationQuitting()) return;if (TimeManager.Instance == null) return;float currentHour = TimeManager.Instance.GetTotalHours();float alpha = CalculateAlpha(currentHour);float emission = CalculateEmission(currentHour);Debug.Log($"[Cloud Debug] 当前时间: {currentHour:F2}\n" +$"透明度: {alpha:F2}, 发射率: {emission:F2}\n" +$"激活: {isActive}, 粒子数: {particleSys.particleCount}");}[ContextMenu("强制刷新云层")]public void ForceRefreshCloud(){if (Singleton<TimeManager>.IsApplicationQuitting()) return;UpdateCloudState();}
}
4、夜晚点光源

(1) Player上右键,Light-Point Light,Position.y = 0.2,调整Range为 5

(2) 给该点光源添加 PlayerFootLightController.cs

using UnityEngine;[RequireComponent(typeof(Light))]
public class PlayerFootLightController : MonoBehaviour
{[Header("光照设置")]public float nightIntensity = 1.5f;public float dayIntensity = 0.2f;public Color nightColor = new Color(0.8f, 0.9f, 1f, 1f); // 淡蓝色public Color dayColor = new Color(1f, 0.9f, 0.7f, 1f);   // 暖黄色public bool disableInDay = true; [Header("过渡效果")]public float transitionSpeed = 2f;public float minDistanceFromGround = 0.2f;public float maxDistanceFromGround = 2f;[Header("射线检测设置")]public LayerMask groundLayer;         // 手动指定地面层级public float raycastMaxDistance = 5f; // 增大射线长度,适配跳跃private Light footLight;private Transform playerTransform;private bool isDayTime;private float targetIntensity;private Color targetColor;private void Awake(){footLight = GetComponent<Light>();playerTransform = transform.parent;if (playerTransform == null){Debug.LogError("PlayerFootLightController:点光源必须作为玩家对象的子对象!");enabled = false; // 禁用脚本,避免后续空引用return;}// 初始状态(后续Start会根据TimeManager覆盖)footLight.intensity = nightIntensity;footLight.color = nightColor;footLight.enabled = true;// 校验地面层级(编辑器提醒)if (groundLayer.value == 0){Debug.LogWarning("PlayerFootLightController:请在Inspector中指定地面LayerMask!");}}private void Start(){if (TimeManager.Instance != null){isDayTime = TimeManager.Instance.IsDayTime;TimeManager.Instance.OnDayNightChange += HandleDayNightChange;TimeManager.Instance.OnSunPositionUpdate += HandleSunPositionUpdate;UpdateLightState(isDayTime);}else{Debug.LogWarning("PlayerFootLightController:未找到TimeManager实例,光照将保持初始夜间状态!");}}private void OnDestroy(){if (Singleton<TimeManager>.IsApplicationQuitting()) return;if (TimeManager.Instance != null){TimeManager.Instance.OnDayNightChange -= HandleDayNightChange;TimeManager.Instance.OnSunPositionUpdate -= HandleSunPositionUpdate;}}private void Update(){// 平滑过渡footLight.intensity = Mathf.Lerp(footLight.intensity, targetIntensity, Time.deltaTime * transitionSpeed);footLight.color = Color.Lerp(footLight.color, targetColor, Time.deltaTime * transitionSpeed);// 白天禁用光照(可选)if (isDayTime && disableInDay){footLight.enabled = false;}else{footLight.enabled = footLight.intensity > 0.01f; // 强度极低时也禁用,避免浪费性能}UpdateLightPosition();}private void HandleDayNightChange(bool isDayTime){this.isDayTime = isDayTime;UpdateLightState(isDayTime);}private void HandleSunPositionUpdate(float sunHeight){// 校验sunHeight范围(避免异常值)sunHeight = Mathf.Clamp01(sunHeight);float intensityModifier = 1f - sunHeight;targetIntensity = (isDayTime ? dayIntensity : nightIntensity) * intensityModifier;}private void UpdateLightState(bool isDay){if (isDay){targetIntensity = disableInDay ? 0 : dayIntensity; // 白天禁用则强度设为0targetColor = dayColor;}else{targetIntensity = nightIntensity;targetColor = nightColor;}}private void UpdateLightPosition(){if (playerTransform == null) return;Vector3 rayStart = playerTransform.position + Vector3.up * 1f;if (Physics.Raycast(rayStart, Vector3.down, out RaycastHit hit, raycastMaxDistance, groundLayer)){float intensityRatio = Mathf.InverseLerp(0, nightIntensity, targetIntensity);float distanceAboveGround = Mathf.Lerp(minDistanceFromGround, maxDistanceFromGround, intensityRatio);transform.position = hit.point + Vector3.up * distanceAboveGround;}else{// 未检测到地面时,跟随玩家脚下(添加Y偏移,避免陷进地面)transform.position = playerTransform.position + Vector3.up * minDistanceFromGround;}}// 调试方法:手动切换光照状态public void ToggleLight(){isDayTime = !isDayTime;UpdateLightState(isDayTime);}// 调试方法:强制更新状态public void RefreshLight(){if (TimeManager.Instance != null){isDayTime = TimeManager.Instance.IsDayTime;UpdateLightState(isDayTime);}}// 在编辑器中可视化射线检测private void OnDrawGizmosSelected(){if (playerTransform == null) return;// 射线可视化Gizmos.color = Color.yellow;Vector3 rayStart = playerTransform.position + Vector3.up * 1f;Gizmos.DrawLine(rayStart, rayStart + Vector3.down * raycastMaxDistance);// 光照位置可视化if (footLight != null){Gizmos.color = footLight.color;Gizmos.DrawWireSphere(transform.position, footLight.range * 0.1f);}}
}

(3) 配置参数

八、星空
(一)基础星星粒子系统
1、发光材质

(1) 新建材质 `StarParticleMat`

(2) Shader选择 `Universal Render Pipeline/Particles/unlit`

(3) 参数设置:

① Surface Type: Transparent

② Blending Mode: Additive

③ Base Map : 选择Default Particle

④ Color Mode: Multiply

⑤ 启用 Emission,Emission Color :  淡蓝色 (0.5,0.7,1.0 #)          Intensity(强度)2.0

2、粒子系统

(1) Effects → Particle System, 命名为 ProceduralStars

(2) 层级和Transform

 

(3) 添加脚本

① 给ProceduralStars 添加 Assets/Scripts/Environment/StarParticleController.cs组件

using UnityEngine;[RequireComponent(typeof(ParticleSystem))]
public class StarParticleController : BaseParticleController
{[Header("星星基础设置")]public bool isConstellation = false;[Range(0, 500)] public float maxEmissionRate = 100f;[Range(0, 1)] public float minBrightness = 0.1f;[Range(0, 5)] public float maxBrightness = 2.0f;public Color starColor = new Color(0.5f, 0.7f, 1.0f);[Header("星座专属设置")][Range(0, 50)] public float constellationMaxRate = 5f;[Range(18, 24)] public int startHour = 18;[Range(0, 6)] public int endHour = 6;[Range(0, 10)] public float fadeDuration = 2f;[Header("太阳高度淡出设置")]public float fadeStartHeight = 0.3f;public float fadeEndHeight = 0.5f;[Header("星光呼吸效果(仅主星星生效)")][Tooltip("呼吸速度(数值越小越慢)")][Range(0.1f, 2f)] public float breathSpeed = 0.5f;[Tooltip("最大亮度倍数(避免过曝)")][Range(1f, 3f)] public float maxBrightnessMultiplier = 1.8f;private Camera mainCamera;[Range(100f, 500f)] public float breathMaxDistance = 300f; // 超过300米不呼吸private ParticleSystem.EmissionModule emission;private Material starMat;private SunManager sunManager;private float targetEmissionRate;private float currentEmissionRate;private float lastBreathTime = 0f;private Color originalStarColor;private bool isBreathEnabled = true; // 控制呼吸效果是否启用protected override void Awake(){base.Awake();if (particleSys == null){Debug.LogError($"[{gameObject.name}] 未找到ParticleSystem组件!", this);enabled = false;return;}emission = particleSys.emission;originalStarColor = starColor;// 初始化材质(用于呼吸效果和亮度控制)if (particleRenderer != null && particleRenderer.material != null){starMat = particleRenderer.material;ConfigureRenderingOrder();}else{Debug.LogWarning($"[{gameObject.name}] 未找到材质,呼吸效果和亮度控制失效!", this);isBreathEnabled = false;}// 初始状态targetEmissionRate = 0;currentEmissionRate = 0;emission.rateOverTime = 0;// 星座不启用呼吸效果(仅主星星生效)if (isConstellation)isBreathEnabled = false;mainCamera = Camera.main;if (mainCamera == null){Debug.LogWarning($"[{gameObject.name}] 未找到主相机,呼吸效果的距离衰减功能失效!", this);}}protected override void OnEnable(){base.OnEnable();sunManager = FindObjectOfType<SunManager>();if (sunManager == null){Debug.LogError($"[{gameObject.name}] 未找到SunManager!星星渐变功能失效", this);}}protected override void OnDestroy(){base.OnDestroy();starMat = null;}private void Update(){// 发射率平滑过渡currentEmissionRate = Mathf.MoveTowards(currentEmissionRate,targetEmissionRate,(maxEmissionRate / fadeDuration) * Time.deltaTime);emission.rateOverTime = currentEmissionRate;// 星座特殊逻辑if (isConstellation && currentEmissionRate <= 0 && particleSys.isPlaying){particleSys.Stop(true, ParticleSystemStopBehavior.StopEmitting);}// 呼吸效果(仅夜晚+主星星+材质存在时生效)UpdateBreathEffect();}// 呼吸效果核心逻辑private void UpdateBreathEffect(){if (Vector3.Distance(transform.position, mainCamera.transform.position) > breathMaxDistance)return;if (Time.time - lastBreathTime < 0.05f) return; // 每0.05秒更新一次lastBreathTime = Time.time;if (!isBreathEnabled || starMat == null || !particleSys.isPlaying || IsDayTime())return;// 计算呼吸因子(0~1周期性波动)float breathFactor = Mathf.PingPong(Time.time * breathSpeed, 1.0f);// 计算最终亮度(基础亮度 + 呼吸波动)float baseEmissionValue = Mathf.Lerp(minBrightness, maxBrightness, GetNightBrightnessFactor());float finalEmissionValue = baseEmissionValue * (1 + (breathFactor * (maxBrightnessMultiplier - 1)));// 应用到材质starMat.SetColor("_EmissionColor", originalStarColor * finalEmissionValue);starMat.EnableKeyword("_EMISSION");}// 获取夜晚亮度因子(结合太阳高度)private float GetNightBrightnessFactor(){if (sunManager == null) return 1f;float fadeFactor = Mathf.Clamp01((sunManager.SolarAltitude - fadeStartHeight) / (fadeEndHeight - fadeStartHeight));return 1f - fadeFactor;}// 判断是否为白天(用于关闭呼吸效果)private bool IsDayTime(){return sunManager != null ? sunManager.SolarAltitude >= fadeEndHeight : false;}protected override void HandleSunPositionUpdate(float sunHeight){base.HandleSunPositionUpdate(sunHeight);if (sunManager == null || starMat == null) return;// 太阳高度淡出逻辑float fadeFactor = Mathf.Clamp01((currentSunHeight - fadeStartHeight) / (fadeEndHeight - fadeStartHeight));float brightnessFactor = 1f - fadeFactor;if (particleRenderer.enabled){// 更新基础亮度(结合呼吸效果的基础值)float emissionValue = Mathf.Lerp(minBrightness, maxBrightness, brightnessFactor);starMat.SetColor("_EmissionColor", originalStarColor * emissionValue);starMat.EnableKeyword("_EMISSION");// 更新粒子透明度var mainModule = particleSys.main;Color particleColor = mainModule.startColor.color;particleColor.a = brightnessFactor;mainModule.startColor = particleColor;}}protected override bool CalculateShouldBeActive(float currentHour, float sunriseHour,float sunsetHour, float disappearBeforeSunriseHours, float appearAfterSunsetHours){if (isConstellation){return currentHour >= startHour || currentHour < endHour;}return base.CalculateShouldBeActive(currentHour, sunriseHour, sunsetHour,disappearBeforeSunriseHours, appearAfterSunsetHours);}protected override void SetParticleState(bool active){base.SetParticleState(active);targetEmissionRate = active ? (isConstellation ? constellationMaxRate : maxEmissionRate) : 0;if (active && particleSys.isPaused) particleSys.Play();}private void ConfigureRenderingOrder(){if (particleRenderer == null || particleRenderer.material == null) return;particleRenderer.material.renderQueue = 3001;particleRenderer.sortingOrder = 1;particleRenderer.material.SetInt("_ZWrite", 0);}[ContextMenu("刷新星星状态")]public void RefreshStarState(){RefreshState();if (sunManager != null){HandleSunPositionUpdate(sunManager.SolarAltitude);}}
}

② 给ProceduralStars 添加 ParticleCulling.cs组件,视野外不渲染

3、参数配置

#DAF0FF

#80B2FF → #FFFFFF  → #80B2FF ,204 →255 →204

#E6DCF7→ #FFFFFF →#E6DCF7,  200→ 255→200

主要参数

(1) Duration(持续时间):100000 (永远存在)       

(2) StartSpeed:0.01  (粒子初始发生时候的速度)

(3) StartSize:0.3-0.5 (粒子初始的大小)

(4) StartColor:淡蓝色 (粒子初始颜色,可以调整加上渐变色  #DAF0FF)

(5) MaxParticles:75

(6) Start Rotation : 30

​​​​

Emission

(1) RateOverTime:5 (单位时间生成的粒子数)

(2) Bursts

① Time:0(从第几秒开始)

② Count:1【在指定时间点(Time参数设定的时刻)一次性发射的粒子数量】

③ Cycles:1(在一个周期中循环的次数)

Shape

(1) Shape:Cone (发射体积的形状)

(2) Radius:20(形状的圆形半径)

(3) Radius Thickness:0.99(值越接近 1,粒子越贴近球体外表面)

(发射粒子的体积比例。0 表示从形状的外表面发射粒子。值为 1 表示从整个体积发射粒子)

​​​​

Velocity over Lifetime  (使粒子缓慢向外扩散,增强空间感)Linear: (0, 0, 0.1),speed:0.01

Inherit Velocity     Multiplier:0.2(粒子应该继承的发射器速度的比例)

Color over Lifetime      粒子在其生命周期内的颜色渐变

① 设置:Alpha通道:轻微波动(0.8 → 1 → 0.8)

② 说明:渐变条的左侧点表示粒子寿命的开始,而渐变条的右侧表示粒子寿命的结束

③ 颜色渐变:淡蓝(0.5,0.7,1.0 #80B2FF) → 白(1,1,1) → 淡蓝

勾选 Random Between Two Colors,设置两个接近的颜色范围(如冷白→暖白)

Size over Lifetime

① 设置:选择 Random Between Two Constants 0.8~1.2

② 说明:每个粒子独立变化

或:   - 模式: Curve       - 曲线: 0.0(0.8) → 0.5(1.2) → 1.0(0.8)
Noise(模拟星光的轻微动态偏移)

① Strength:0.01~0.03(值越高,粒子移动越快和越远。)

② Frequency: 0.5
③ Scroll Speed: 0.1

Lights 
① Ratio: 0.3
② Intensity: 0.5-1.0
③ Range: 5.0
RendererRender Mode 选 Billboard

Material 选择上面创建的自发光材质StarParticleMat

​​​​

(二)添加星座点缀
1、新建子粒子系统
  • 右键  →  Create Empty ChildConstellations,命名为 Constellations

  • 添加组件 Particle System

2、特殊参数设置

#C7D9D9  →  #CC88FF

#C7A7DC→#FFFFFF    →  #C7A7DC, 178 → 255   →  178

Size over Lifetime:曲线:0.0(0.5) → 0.3(1.5) → 1.0(0.5)

(1) 主要参数

- Start Size: 0.02-0.04

- Start Color: 选择渐变颜色 #C7D9D9 → #CC88FF

- StartSpeed:0.01

- MaxParticles:50

(2) Emission
- Bursts: Count=5, Interval=0.01

(3) Shape:同父粒子

(4) Renderer
- Sorting Layer: Default
- Order in Layer: 1 (确保在父系统上层)

- 材质使用 `StarParticleMat` 

(5) Color over Lifetime:

颜色渐变:紫(0.8,0.5,1.0) → 白 → 紫(C7A7DC), Alpha通道:0.7 → 1.0 → 0.7

(6) Size over Lifetime:曲线:0.0(0.5) → 0.3(1.5) → 1.0(0.5)

4、添加脚本

(1) 给子粒子系统 Constellations 添加 Assets/Scripts/Environment/StarParticleController.cs

勾选 Is Constellation

http://www.dtcms.com/a/613457.html

相关文章:

  • MinIO 不再“开放”,RustFS 能否成为更优选择?
  • DMLDCL
  • 大型ERP管理系统多语言分层架构设计
  • WordPress网站404公益页面公司网站建设策划书
  • B-树分析
  • 关于做网站建设公司你应该知道的宣传网站建设方案
  • VSCode 1.106 版本发布 —— 更强 AI 特性,更丝滑的编程体验!
  • F046 新闻推荐可视化大数据系统vue3+flask+neo4j
  • SpringMVC基础教程(3)--SSM框架整合
  • 1.硬件测试测试方案设计方法
  • 个人网站名字大全大学生创意产品设计
  • 基于 **Three.js** 开发的 3D 炮弹发射特效系统
  • 前端构建工具缓存清理,npm cache与yarn cache
  • 【开题答辩全过程】以 翡翠仓库管理系统为例,包含答辩的问题和答案
  • 2025 批量下载微博内容/图片/视频,导出word和pdf,微博点赞/评论/转发等数据导出excel
  • 高级网站开发工程师证书天眼查网站建设公司
  • 11.3 实战:使用FastGPT开发企业级智能问答Agent
  • Spring AI接入DeepSeek:构建你的第一个AI应用
  • 中国最大免费wap网站wordpress转代码
  • Unable to load class ‘org.slf4j.LoggerFactory‘.解决
  • 2025年印尼服务器选型指南:跨境业务落地的合规与性能双解
  • 【C++】C++11:右值引用和移动语义
  • 【ZeroRange WebRTC】视频文件RTP打包与发送技术深度分析
  • 上海网站建设网站开发seo矩阵培训
  • 动手实践:安装Docker并运行你的第一个Web应用
  • 入门C语言编译器 | 学习如何选择和配置C语言开发环境
  • 开源asp学校系统网站跨境电商平台有哪些免费的
  • 前端构建工具多页面配置,Webpack与Vite
  • 茂名网站建设服务怀柔高端网站建设
  • Photoshop图层样式