返回作品索引WORK / CASE STUDY
工具

URP - RendererFeature :体积云

URP体积云效果学习

独立作品

让周围轻一点,文字近一点。
作品演示点击播放,可全屏观看
ENGINE
Unity · URP
TECH
rendering · 后处理 · Shader · unity · URP · Raymarching · Compute Shader · RenderFeature
ROLE
工具
YEAR
2026
案例目录57 小节

云的本质是:在 3D 空间中,哪里有介质,有多少。 体积雾用的是 exp(-height) * noise,很粗糙。云需要更精确地描述”蓬松感”和”层次感”,工业标准答案是两层噪声 + Weather Map 控制。需要离线预计算两张 3D Texture。

云的种类

这是 Nubis 系统里定义的”对流层云”分类图:

英文中文高度层
Cirrostratus卷层云高层(~8000m)
Cirrus卷云高层(~8000m)
Altostratus高层云中层(~4000m)
Altocumulus高积云中层(~4000m)
Stratus层云低层(~1500m)
Stratocumulus层积云低层(~1500m)
Cumulus积云低层(~1500m)
Cumulonimbus积雨云全层(1500m~8000m+)

云的梯度分布构建

TopGradient

这里实际上是根据高度,定义了三类云的梯度,然后进行组合,这里图有点问题,实际上,上层渐变和下层渐变是分离处理了

组合后,由TopType参数进行控制,范围0~1

BottomGradient

同理也是构建一个底层渐变

因为不清楚官方是手绘的还是怎么生成的,这里我采用比较直接的梯度构建,仅使用了三种类型来近似

梯度合并

合并公式,这里引入了CloudCoverage来控制云的整体密度扩散

代码参考,最开始我这里使用了max进行过渡,虽然过渡比较好,但是这样会导致底部的侵蚀不明显

最终还是选择直接进行插值

球壳步进范围判断

普通的步进范围限制是进行AABB的范围来判断,但是球壳范围判断会复杂一些 射线上的某个点的表达式是:

r(t)=o+t⋅dr(t) = o + t·d

那么假设点打在了球壳上,那么就会有:

∣O+D⋅t−C∣2=r2|O + D·t − C|² = r²
  • C是表示球心
  • r则是半径
  • D是步进单位
  • t是步进次数

球壳上的点到球心的距离是r,也就是半径,根据这个限定条件,就可以求出t

因为原点O表示相机位置,C是球心位置,这里可以先进行简化,直接可以合并成相机到球心的距离

L=O−CL=O-C ∣L+D⋅t∣2=r2|L + D·t|² = r²

然后就是展开化简

L⋅L+2t(D⋅L)+t2(D⋅D)=r2L·L + 2t(D·L) + t²(D·D)= r²

然后移项,这样就得到了关于t的

(D⋅D)⋅t2+2(D⋅L)⋅t+(L⋅L−r2)=0(D·D)·t² + 2(D·L)·t + (L·L − r²) = 0

这里因为D是单位向量,所以平方之后直接是1了,现在的形式和下面的形式类似,那么就是求方程解

t2+2bt+c=0t² + 2bt + c = 0

最后t的表达式就是,这里需要限定b² − c > 0

t=−b±(b2−c)t = −b ± \sqrt{(b² − c)}

这里把b和c代入就可以求得t的具体值,这两个值表示入球点和出球点 参考代码:

因为解出来有两个值,所以还要比对一下,计算最终的tmin和tmax,可以参考下面这个图

CloudShadpeNoise

FBM

FBM 全称 Fractional Brownian Motion,分形布朗运动,来自数学里对自然界随机现象的建模。 直觉理解 自然界的很多东西都有多尺度的细节: 山脉有大山脉,大山脉上有小山丘,小山丘上有石头,石头上有纹路——每个尺度上都有相似的粗糙感。 云也一样:大团云里有中等云块,中等云块上有小蓬松结构。 比如说单层 Worley 只有一个尺度,看起来太规则。FBM 就是把多个尺度叠加来模拟这种自然感。

fbm = noise(p * 1.0) * 0.500   // 低频:大结构,权重高
    + noise(p * 2.0) * 0.250   // 中频:中结构,权重中
    + noise(p * 4.0) * 0.125   // 高频:小细节,权重低

Worley噪声

下图是Worley噪声

ComputerShaderWorley噪声的生成代码参考:

FBM处理函数

Perlin噪声

ComputerShaderPerlin噪声生成参考代码:

FBM处理函数

Perlin-Worley合成

Shape Noise的格式是(128³,RGBA),各通道如下。

R: Perlin-Worley 噪声(主导云的大轮廓)
G: Worley F1 噪声(低频)
B: Worley F1 噪声(中频)
A: Worley F1 噪声(高频)

ComputerShader中的Noise合成公式如下:

对应的生成混合代码

Shader的采样混合方式

CloudDetailNoise

Worley

CloudShadpeNoise也就是Perlin Worley构成云的基本形状,然后CloudDetailNoise就是使用多个频率的低分辨率Worley噪声来进行侵蚀

Worley侵蚀图的生成代码参考:

Noise采样

CloudShadpe

采样ShadpeNoise来得到基础的云型

参考代码

基础的云形状,还需要和梯度进行合并,这样根据云种高差才会不一样

参考代码:

DetailNoise

得到云的基本形状之后,通过低分辨率的WorlyNoise进行侵蚀,得到不规则的边缘,效果如下,这里是其它功能添加后的效果

但看形状的话是下面这样,可以忽略最后面的后面卷云

参考代码

光照计算

现在有了云的基本形状,但是没有自阴影,这里需要从光照入手了 官方在这里分为直接光和间接光两个权重,来控制云的自阴影,在这之前先得理解基础的散射,也就是比尔-朗伯散射

比尔-朗伯散射和简化

标准的比尔-朗伯

标准的比尔朗伯,是指数衰减的,意思是光在介质中传播是指数衰减,这跟密度有关

因为是光线步进,那么每次步进的光线衰减的计算代码是这样的

d = exp(-σ * stepSize) // σ 是消光系数
// 转换成下面的代码
float extinctionCoeff = density * _Extinction;
float stepTransmittance = exp(-extinctionCoeff * stepSize);

每次步进的衰减权重有了,然后进行累积就得到整体的衰减权重

transmittance(累积) = exp(-σ₁s) * exp(-σ₂s) * ...
// 透射率累积 换成代码也就是
transmittance *= stepTransmittance;

简化

对于标准的比尔-朗伯累积的我是这样理解的:

每步的贡献=基础权重×密度每步的贡献 = 基础权重 × 密度
// 这里对应
基础权重 = stepSize  (这一步覆盖的空间厚度)
密度     = 采样得到的 σ(独立的,每步不同)

因为介质不均匀,这表示在每层的stepSize是不是均匀分布的,所以基础权重采用的指数衰减,也就是:

d = exp(-σ * stepSize) 

那么精简的话就直接假设是均匀分布就行了,如果理解成近似均匀介质,那么每层的基本权重也就是下面这样

d ≈ density * stepSize (线性近似)
// 当然这里可以加上消光系数
d ≈ density * stepSize * _Extinction(线性近似)

因为现在每层的基本权重相,因为现在衰减只和密度相关,而密度增加透射率减小所以效果是相反的,所以要先翻转,那么每次步进的衰减比例就是saturate(1.0 - d)

float d = density * stepSize * _Extinction;
float stepTransmittance = saturate(1.0 - d);
transmittance *= stepTransmittance;

关于1.0 - d这里其实容易产生负值,虽然钳制可以解决,但是精度会差,2022官方是算法这里将这里变成加法计算,先直接计算吸收率,然后翻转就可以得到透射率

散射率累积就是1-吸收率

// 初始值
float light_absorption = 0f;
float d = density * stepSize * _Extinction;
// 吸收比例累积
light_absorption += d * (1.0 - light_absorption);
// 透射率累积
transmittance = 1 - light_absorption;

Direct Scattering

关于直接光的权重又分为下面几项,前半可以表示光线穿透程度,而后半则是体积透光程度,有点类似菲涅尔

 LightTransmittance

在透射率计算中,得到云的形状,这个是从相机开始步进这也就是主循环,体积雾就是这样,但雾和云是不一样的,云底部是黑的,它的遮蔽更强,这个就需要从灯光进行步进计算主光源的透射率

官方这里的是选取了10个点进行步进,每次步长翻倍

这里参考代码如下,我这里只取了6个点,实现方式和官方的有点区别,后期调整性能,可能会修改

Primary Scattering

标准的比尔-朗伯Beer-Lambert是指数衰减,但是这样实际上云层会过暗 官方为了解决满足视觉,对这个散射进行了调整

LightTransmittance这里是直接同参数时的对比

这里代码对应Primary Scattering,除此之外还有phase,实际对应是相位函数

相位函数

对于单向的海尼-格林斯坦相位函数,不能较好的体现云透光的效果

所以这里使用了双向的相位函数,其实就是跑两边,背侧的给了强度控制,效果其实就是银边的效果

参考代码

那么Direct Scattering的前半部分就是这样

当前的效果如下,可以调整云的透光和银边效果强度

在早期版本,模拟云的体积透光效果,发现刚好与散射公式相反

Multiple Scattering

这个效果也被叫做powderEffect糖粉外观

所以最初的处理就是powderEffect

在后面改进了算法,变成了这样

在2022版nubis中,这里将其整合成了Multiple Scattering

AmbientScattering

最后就是环境光部分

卷云层

上述的处理,都是积云层的处理,也就是3D云,在高处的卷云因为非常薄,所以作为2.5D云来处理

密度模型

薄云随Type进行切换

卷云光照处理

因为卷云很薄,所以只要跑一次循环就行,关于光线的衰减,和积云的光照几乎相同

sampleCirrusLightTransmittance函数参考代码:

采样参考代码

双相 Raymarching

自适应步长

HeightProfile

之前的构建方法是使用代码直接构建梯度,为了方便,直接烘焙成贴图了

大致界面如下,这里改成了渐变条来编辑

HeightProfileGenerator

生成代码

using UnityEngine;

[ExecuteAlways]
public class HeightProfileGenerator : MonoBehaviour
{
    public static HeightProfileGenerator Instance { get; private set; }

    [Header("六条 fade 渐变,编辑时用,运行时不再直接采样")]
    public Gradient stratusBottom       = LinearGradient(0f, 0f, 0.03f, 1f);
    public Gradient stratusTop          = LinearGradient(0.15f, 1f, 0.25f, 0f);
    public Gradient stratocumulusBottom = LinearGradient(0.03f, 0f, 0.15f, 1f);
    public Gradient stratocumulusTop    = LinearGradient(0.35f, 1f, 0.65f, 0f);
    public Gradient cumulusBottom       = LinearGradient(0.1f, 0f, 0.25f, 1f);
    public Gradient cumulusTop          = LinearGradient(0.6f, 1f, 0.95f, 0f);

    [Header("烘焙设置")]
    public int resolution = 128;   // 现在是二维 LUT 的边长,不再是"6行"的那个128

    // 运行时资源:resolution × resolution,单通道,x=cloudType, y=heightFraction
    public Texture2D profileLUT { get; private set; }

    bool isDirty = true;

    static Gradient LinearGradient(float t0, float v0, float t1, float v1)
    {
        var g = new Gradient();
        g.mode = GradientMode.Blend;
        g.SetKeys(
            new[]
            {
                new GradientColorKey(new Color(v0, v0, v0), t0),
                new GradientColorKey(new Color(v1, v1, v1), t1)
            },
            new[]
            {
                new GradientAlphaKey(1f, 0f),
                new GradientAlphaKey(1f, 1f)
            });
        return g;
    }

    void OnEnable()  { Instance = this; Bake(); }
    void OnDisable() { if (Instance == this) Instance = null; }
    void OnValidate() { isDirty = true; }

    void Update()
    {
        if (isDirty) { Bake(); isDirty = false; }
    }

    public Gradient[] Gradients => new[]
    {
        stratusBottom, stratusTop,
        stratocumulusBottom, stratocumulusTop,
        cumulusBottom, cumulusTop
    };

 
    // 原来 GetHeightGradient 的混合逻辑,现在只在烘焙时跑一次,不再进 shader
    public float EvaluateHeightGradient(float heightFraction, float cloudType)
    {
        var g = Gradients;
        float topType    = Mathf.Clamp01(cloudType);
        float bottomType = Mathf.Clamp01(cloudType);

        float stratusBottomFade       = g[0].Evaluate(heightFraction).r;
        float stratusTopFade          = g[1].Evaluate(heightFraction).r;
        float stratocumulusBottomFade = g[2].Evaluate(heightFraction).r;
        float stratocumulusTopFade    = g[3].Evaluate(heightFraction).r;
        float cumulusBottomFade       = g[4].Evaluate(heightFraction).r;
        float cumulusTopFade          = g[5].Evaluate(heightFraction).r;

        // float t1 = Mathf.Clamp01(topType * 2f);
        // float t2 = Mathf.Clamp01((topType - 0.5f) * 2f);
        float t1 = Mathf.SmoothStep(0f, 1f, Mathf.Clamp01(topType * 2f));
        float t2 = Mathf.SmoothStep(0f, 1f, Mathf.Clamp01((topType - 0.5f) * 2f));

        // float topGradient =  Mathf.Lerp(stratusTopFade,  Mathf.Max(stratusTopFade, stratocumulusTopFade), t1);
        // topGradient =  Mathf.Lerp(topGradient,  Mathf.Max(topGradient, cumulusTopFade), t2);

        float topGradient = Mathf.Lerp(
            Mathf.Lerp(stratusTopFade, stratocumulusTopFade, t1),
            cumulusTopFade, t2);

        // float b1 = Mathf.Clamp01(bottomType * 2f);
        //float b2 = Mathf.Clamp01((bottomType - 0.5f) * 2f);
        float b1 = Mathf.SmoothStep(0f, 1f, Mathf.Clamp01(bottomType * 2f));
        float b2 = Mathf.SmoothStep(0f, 1f, Mathf.Clamp01((bottomType - 0.5f) * 2f));

        // float  bottomGradient = Mathf.Lerp(stratusBottomFade,  Mathf.Max(stratusBottomFade, stratocumulusBottomFade), b1);
        // bottomGradient = Mathf.Lerp(bottomGradient,  Mathf.Max(bottomGradient, cumulusBottomFade), b2);

        float bottomGradient = Mathf.Lerp(
            Mathf.Lerp(stratusBottomFade, stratocumulusBottomFade, b1),
            cumulusBottomFade, b2);

        return Mathf.Clamp01(topGradient * bottomGradient);
    }
    
    public void Bake()
    {
        if (profileLUT == null || profileLUT.width != resolution || profileLUT.height != resolution)
        {
            if (profileLUT != null) DestroyImmediate(profileLUT);
            profileLUT = new Texture2D(resolution, resolution, TextureFormat.R8, false);
            profileLUT.wrapMode = TextureWrapMode.Clamp;
            profileLUT.filterMode = FilterMode.Bilinear;
        }

        for (int y = 0; y < resolution; y++)
        for (int x = 0; x < resolution; x++)
        {
            float heightFraction = y / (float)(resolution - 1);
            float cloudType      = x / (float)(resolution - 1);
            float v = EvaluateHeightGradient(heightFraction, cloudType);
            profileLUT.SetPixel(x, y, new Color(v, v, v, v));
        }
        profileLUT.Apply();
    }
}

HeightProfileGeneratorEditor

编辑Editor,方便预览

#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(HeightProfileGenerator))]
public class HeightProfileGeneratorEditor : Editor
{
    public override void OnInspectorGUI()
    {
        DrawDefaultInspector();

        var gen = (HeightProfileGenerator)target;
        if (gen.profileLUT == null) return;

        EditorGUILayout.Space(12);
        EditorGUILayout.LabelField("合成预览(cloudType × height)", EditorStyles.boldLabel);
        EditorGUILayout.HelpBox("profileLUT 是 R8 单通道纹理,IMGUI 直显会偏红,不影响实际数据,仅预览观感。",
            MessageType.None);

        var compRect = GUILayoutUtility.GetRect(280, 280, GUILayout.MaxWidth(280));
        GUI.DrawTexture(compRect, gen.profileLUT, ScaleMode.ScaleToFit);

        var axisRect = GUILayoutUtility.GetRect(280, 16);
        EditorGUI.LabelField(axisRect, "→ cloudType", EditorStyles.miniLabel);
        EditorGUILayout.LabelField("↑ height(图内竖轴,底部=0,顶部=1)", EditorStyles.miniLabel);
    }
}
#endif

Weather

GenerateWeatherMap

这里ComputerShader其实就是生成2D的PerlinNoise来作为WeatherMap

#pragma kernel GenerateWeatherMap

RWTexture2D<float4> _WeatherMap;
int _Resolution;
float _WeatherMapScale;
float _CoverageBias;
float _CoverageScale;
float _CloudTypeBias;
float _CloudTypeScale;
float _CirrusCoverageBias;
float _CirrusCoverageScale;
float _CirrusTypeBias;
float _CirrusTypeScale;
float _Seed;

// ============================================================
// 整数 hash:任意大坐标下精度稳定
// ============================================================
float hash2(float2 p, float seed)
{
    uint2 q = uint2(int2(floor(p)) + 32768);
    uint h = q.x * 1664525u + q.y * 1013904223u;
    h += (uint)(seed * 373.0);          // seed 混入整数域
    h = (h ^ (h >> 16)) * 2654435769u;
    h = h ^ (h >> 16);
    return float(h) * (1.0 / 4294967295.0);   // [0, 1]
}

float PerlinNoise(float2 p, float seed)
{
    float2 i = floor(p);
    float2 f = frac(p);
    float2 u = f * f * (3.0 - 2.0 * f);

    // hash 出角度 -> 单位梯度向量,天然归一化,
    float a00 = hash2(i + float2(0, 0), seed) * 6.2831853;
    float a10 = hash2(i + float2(1, 0), seed) * 6.2831853;
    float a01 = hash2(i + float2(0, 1), seed) * 6.2831853;
    float a11 = hash2(i + float2(1, 1), seed) * 6.2831853;

    float result = lerp(
        lerp(dot(float2(cos(a00), sin(a00)), f - float2(0, 0)),
             dot(float2(cos(a10), sin(a10)), f - float2(1, 0)), u.x),
        lerp(dot(float2(cos(a01), sin(a01)), f - float2(0, 1)),
             dot(float2(cos(a11), sin(a11)), f - float2(1, 1)), u.x),
        u.y
    );
    return result * 0.5 + 0.5;
}

// 旋转矩阵(约36.87度,octave 间去相关)
static const float2x2 rot = float2x2(0.80, 0.60, -0.60, 0.80);

float fbm(float2 p, int octaves, float lacunarity, float gain)
{
    float value = 0.0;
    float amplitude = 0.5;
    float2 shift = float2(100.0, 100.0);

    [unroll]
    for (int i = 0; i < 4; i++)
    {
        if (i >= octaves) break;
        value += PerlinNoise(p, _Seed + float(i)) * amplitude;
        // 每层旋转 + 偏移打破轴向对称;大坐标对整数 hash 无害
        p = mul(rot, p) * lacunarity + shift;
        amplitude *= gain;
    }
    return value;
}

[numthreads(8, 8, 1)]
void GenerateWeatherMap(uint3 id : SV_DispatchThreadID)
{
    float2 uv = float2(id.xy) / float(_Resolution);
    float2 worldXZ = (uv - 0.5) * _WeatherMapScale;

    // R:Coverage(Domain Warping + 旋转 fBm)
    float2 q = float2(
        fbm(worldXZ * _CoverageScale + float2(0.0, 0.0), 3, 2.0, 0.5),
        fbm(worldXZ * _CoverageScale + float2(5.2, 1.3), 3, 2.0, 0.5)
    );
    q = q * 2.0 - 1.0;
    float coverage = fbm((worldXZ + q * 2.0) * _CoverageScale, 4, 2.0, 0.5);
    coverage = saturate(coverage + 0.2 + _CoverageBias);

    // G:CloudType(旋转 fBm)
    float cloudType = fbm(worldXZ * _CloudTypeScale, 3, 2.0, 0.5);
    cloudType = saturate(cloudType +   _CloudTypeBias);
   
    // 改用 smoothstep
    cloudType = smoothstep(0.0, 1.0, cloudType);

    // B:CirrusCoverage
    float2 cq = float2(
        fbm(worldXZ * _CoverageScale * 0.3 + float2(1.7, 9.2), 2, 2.0, 0.5),
        fbm(worldXZ * _CoverageScale * 0.3 + float2(8.3, 2.8), 2, 2.0, 0.5)
    );
    cq = cq * 2.0 - 1.0;
    float cirrusCoverage = fbm((worldXZ + cq * 1.5) * _CirrusCoverageScale, 3, 2.0, 0.5);
    cirrusCoverage = saturate(cirrusCoverage + 0.2 + _CirrusCoverageBias * 0.5);

    // A:CirrusType
    float cirrusType = fbm(worldXZ * _CirrusTypeScale * 0.2, 2, 2.0, 0.5) - 0.5;
    cirrusType = saturate(cirrusType + _CirrusTypeBias);

    _WeatherMap[id.xy] = float4(coverage, cloudType, cirrusCoverage, cirrusType);
}

WeatherMapGenerator

这里脚本负责更新设置WeatherMap的ComputerShader参数

using UnityEngine;

[ExecuteAlways]
public class WeatherMapGenerator : MonoBehaviour
{
    public static WeatherMapGenerator Instance { get; private set; }

    [Header("Compute Shader")]
    public ComputeShader weatherMapCompute;

    [Header("Resolution")]
    public int resolution = 512;

    [Header("World Scale")]
    public float weatherMapScale = 16000f;

    [Header("Coverage")]
    [Range(-0.5f, 0.5f)] public float coverageBias  = 0f;
    public float coverageScale = 0.0001f;

    [Header("Cloud Type")]
    [Range(-0.5f, 0.5f)] public float cloudTypeBias = 0f;
    public float cloudTypeScale = 0.00003f;

    [Header("Cirrus Coverage")]
    [Range(-0.5f, 0.5f)] public float cirrusCoverageBias  = 0f;
    public float cirrusCoverageScale = 0.0001f;

    [Header("Cirrus Type")]
    [Range(-0.5f, 0.5f)] public float cirrusTypeBias  = 0f;
    public float cirrusTypeScale = 0.00003f;


    [Header("Seed")]
    public float seed = 0f;

    public RenderTexture weatherMap { get; private set; }

    private int kernelIndex;
    private bool isDirty = true;

    // 脏标记缓存
    private float lastCoverageBias;
    private float lastCoverageScale;
    private float lastCloudTypeBias;
    private float lastCloudTypeScale;
    private float lastCirrusCoverageBias;
    private float lastCirrusCoverageScale;
    private float lastCirrusTypeBias;
    private float lastCirrusTypeScale;
    private float lastSeed;

    // 手绘刷新
    public bool isPainterOverride = false; 
    void OnEnable()
    {
        Instance = this;
        InitRT();
        if (weatherMapCompute != null)
            kernelIndex = weatherMapCompute.FindKernel("GenerateWeatherMap");
        isDirty = true;
    }

    void Update()
    {
    
        if (isPainterOverride) return;   // 手绘模式下,跳过自动重新生成
        
        if (coverageBias        != lastCoverageBias       ||
            coverageScale       != lastCoverageScale       ||
            cloudTypeBias       != lastCloudTypeBias       ||
            cloudTypeScale      != lastCloudTypeScale      ||
            cirrusCoverageBias  != lastCirrusCoverageBias  ||
            cirrusCoverageScale != lastCirrusCoverageScale ||
            cirrusTypeBias      != lastCirrusTypeBias      ||
            cirrusTypeScale     != lastCirrusTypeScale     ||
            seed                != lastSeed)
        {
            isDirty = true;
        }

        if (isDirty)
        {
            Generate();
            UpdateLastValues();
            isDirty = false;
        }
    }

    void InitRT()
    {
        if (weatherMap != null) weatherMap.Release();

        weatherMap = new RenderTexture(resolution, resolution, 0, RenderTextureFormat.ARGBHalf);
        weatherMap.enableRandomWrite = true;
        weatherMap.useMipMap = true;
        weatherMap.autoGenerateMips = false;
        weatherMap.filterMode = FilterMode.Trilinear;
        weatherMap.wrapMode = TextureWrapMode.Repeat;
        weatherMap.Create();
    }

    public void Generate()
    {
        if (weatherMapCompute == null || weatherMap == null) return;

        weatherMapCompute.SetTexture(kernelIndex, "_WeatherMap", weatherMap);
        weatherMapCompute.SetInt("_Resolution", resolution);
        weatherMapCompute.SetFloat("_WeatherMapScale", weatherMapScale);

        // Coverage
        weatherMapCompute.SetFloat("_CoverageScale", coverageScale);
        weatherMapCompute.SetFloat("_CoverageBias",  coverageBias);

        // Cloud Type
        weatherMapCompute.SetFloat("_CloudTypeBias",  cloudTypeBias);
        weatherMapCompute.SetFloat("_CloudTypeScale", cloudTypeScale);

        // Cirrus Coverage
        weatherMapCompute.SetFloat("_CirrusCoverageBias",  cirrusCoverageBias);
        weatherMapCompute.SetFloat("_CirrusCoverageScale", cirrusCoverageScale);

        // Cirrus Type
        weatherMapCompute.SetFloat("_CirrusTypeBias",  cirrusTypeBias);
        weatherMapCompute.SetFloat("_CirrusTypeScale", cirrusTypeScale);


        // Seed
        weatherMapCompute.SetFloat("_Seed", seed);

        int threadGroups = Mathf.CeilToInt(resolution / 8f);
        weatherMapCompute.Dispatch(kernelIndex, threadGroups, threadGroups, 1);
        weatherMap.GenerateMips(); 
       // Debug.Log($"weatherMap mipmapCount = {weatherMap.mipmapCount}");  
    }

    void UpdateLastValues()
    {
        lastCoverageBias        = coverageBias;
        lastCoverageScale       = coverageScale;
        lastCloudTypeBias       = cloudTypeBias;
        lastCloudTypeScale      = cloudTypeScale;
        lastCirrusCoverageBias  = cirrusCoverageBias;
        lastCirrusCoverageScale = cirrusCoverageScale;
        lastCirrusTypeBias      = cirrusTypeBias;
        lastCirrusTypeScale     = cirrusTypeScale;
        lastSeed                = seed;
    }

    void OnValidate()
    {
        isDirty = true;
        if (weatherMap == null) InitRT();
    }

    void OnDisable()
    {
        if (weatherMap != null)
        {
            weatherMap.Release();
            weatherMap = null;
        }
    }
}

WeatherMapGeneratorEditor

这里对WeatherMapGenerator进行额外设置,预览生成的WeatherMap

#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(WeatherMapGenerator))]
public class WeatherMapGeneratorEditor : Editor
{
    static Material channelMaskMat;
    static RenderTexture[] channelRTs = new RenderTexture[4];

    static readonly Vector4[] channelVectors = {
        new Vector4(1,0,0,0), // R
        new Vector4(0,1,0,0), // G
        new Vector4(0,0,1,0), // B
        new Vector4(0,0,0,1), // A
    };
    static readonly string[] channelLabels = { "Coverage (R)", "CloudType (G)", "CirrusCoverage (B)", "CirrusType (A)" };
    static WeatherMapGenerator pendingGen;

    public override void OnInspectorGUI()
    {
        DrawDefaultInspector();
        var gen = (WeatherMapGenerator)target;
        if (gen.weatherMap == null) return;

        EditorGUILayout.Space(12);
        EditorGUILayout.LabelField("Weather Map 合成预览", EditorStyles.boldLabel);
        var rect = GUILayoutUtility.GetRect(280, 280, GUILayout.MaxWidth(280));
        EditorGUI.DrawPreviewTexture(rect, gen.weatherMap);

        EditorGUILayout.Space(12);
        EditorGUILayout.LabelField("分通道预览", EditorStyles.boldLabel);

        EnsureResources(gen.weatherMap);

        // 只是"预约"一次刷新,真正的Blit挪到下一次EditorApplication.update里做
        pendingGen = gen;
        EditorApplication.update -= DoBlitUpdate;
        EditorApplication.update += DoBlitUpdate;

        EditorGUILayout.BeginHorizontal();
        DrawChannelSlot(0); DrawChannelSlot(1);
        EditorGUILayout.EndHorizontal();
        EditorGUILayout.BeginHorizontal();
        DrawChannelSlot(2); DrawChannelSlot(3);
        EditorGUILayout.EndHorizontal();

        Repaint();
    }

    static void DoBlitUpdate()
    {
        EditorApplication.update -= DoBlitUpdate;
        if (pendingGen == null || pendingGen.weatherMap == null || channelMaskMat == null) return;

        for (int i = 0; i < 4; i++)
        {
            channelMaskMat.SetVector("_ChannelSelect", channelVectors[i]);
            Graphics.Blit(pendingGen.weatherMap, channelRTs[i], channelMaskMat);
        }
    }

    static void EnsureResources(RenderTexture source)
    {
        if (channelMaskMat == null)
        {
            var shader = Shader.Find("Hidden/ChannelMaskPreview");
            if (shader != null) channelMaskMat = new Material(shader);
        }
        for (int i = 0; i < 4; i++)
        {
            if (channelRTs[i] == null || channelRTs[i].width != source.width)
            {
                if (channelRTs[i] != null) channelRTs[i].Release();
                channelRTs[i] = new RenderTexture(source.width, source.height, 0, RenderTextureFormat.ARGB32);
                channelRTs[i].Create();
            }
        }
    }

    static void DrawChannelSlot(int channel)
    {
        EditorGUILayout.BeginVertical(GUILayout.Width(180));
        EditorGUILayout.LabelField(channelLabels[channel], EditorStyles.miniLabel);
        var rect = GUILayoutUtility.GetRect(180, 180, GUILayout.Width(180));
        if (channelRTs[channel] != null)
            EditorGUI.DrawPreviewTexture(rect, channelRTs[channel]);
        EditorGUILayout.EndVertical();
    }
}
#endif

大致预览效果

WeatherPreset

天气预设,预设参数集,用于天气切换。

预设代码

using UnityEngine;

[CreateAssetMenu(fileName = "WeatherPreset", menuName = "Weather/WeatherPreset")]
public class WeatherPreset : ScriptableObject
{
    [Header("===== 云属参数 =====")]
    [Header(">> Stratus Coverage (对流层云)")]
    [Tooltip("Stratus Coverage Value Bias")]
    [Range(-0.5f, 0.5f)] public float coverageBias   = 0f;
    [Tooltip("Stratus Coverage Texture Scale")]
    public float coverageScale  = 0.0001f;
    [Range(-1f, 1f)] public float cloudTypeBias  = 0f;
    public float cloudTypeScale = 0.00003f;

    [Header(">> Stratus Global(对流层云)")]
    [Tooltip("云层底部高度")]
    public float cloudBottom = 400f;
    [Tooltip("云层顶部高度")]
    public float cloudTop = 1500f;

    [Tooltip("cloud Shape Noise")]
    [Range(0f, 100f)]public float shapeNoiseScale = 1f;
    [Tooltip("cloud Detail Noise")]
    [Range(0f, 100f)]public float detailNoiseScale = 1f;

    [Tooltip("cloud Detail Noise Erode Type(侵蚀样式)")]
    [Range(0f, 1f)]public float detailErodeType = 1f;
    [Tooltip("cloud Detail Noise Erode Intensity(侵蚀强度)")]
    [Range(0f, 5f)]public float detailErodeIntensity = 1f;
    [Tooltip("对流云颜色")]
    public Color cloudColor = Color.white;

    [Header(">> Cirrus(卷云)")]
    [Tooltip("Cirrus Coverage Value Bias")]
    [Range(-0.5f, 0.5f)] public float cirrusCoverageBias = 0f;
    [Tooltip("Cirrus Coverage Texture Scale")]
    public float cirrusCoverageScale = 0.0001f;
    [Tooltip("Cirrus Coverage Type Value Bias")]
    [Range(-0.5f, 0.5f)] public float cirrusTypeBias = 0f;
    [Tooltip("Cirrus Coverage Type Texture Scale")]
    public float cirrusTypeScale = 0.00003f;
    
    [Tooltip("卷云高度")]
    [Range(0f, 10000f)] public float cirrusBottom = 6000f; 
    [Tooltip("卷云采样缩放")]
    [Range(0f, 0.001f)] public float cirrusScale = 0.00005f;   
    [Tooltip("卷云亮度")]
    [Range(0f, 2f)] public float cirrusIntensity = 0.3f;     

    [Tooltip("cirrus额外太阳光步进散射吸收系数")]
    [Range(0f, 10f)] public float cirrusAbsorption = 1f;
    [Tooltip("卷云颜色")]
    public Color cirrusColor = Color.white;
    [Header(">> Horizon cloud Fade Settings")]
    [Tooltip("地平线云密度")]
    [Range(0f, 0.1f)] public float aerialDensity = 0.00005f;
    [Tooltip("地平线云颜色(使用天空颜色较好)")]
    public Color horizonTint = Color.white;

    [Header("===== 光照参数 =====")]
    [Header(">> 太阳光")]
    public float sunIntensity = 1f;

    [Header(">> 环境光照")]
    [Tooltip("环境光最低基本限度强度")]
    [Range(0f, 1f)] public float ambientBaseIntensity = 0.1f;
    [Tooltip("云体环境光起始位置")]
    [Range(0f, 1f)] public float ambientFloor = 0.0f;
    [Tooltip("底部环境光强度")]
    [Range(0f, 2f)] public float ambientBottomStrength = 0.2f;
    [Tooltip("糖粉效果强度")]
    [Range(0f, 5f)] public float powderEffectIntensity = 0.1f;
    [Tooltip("环境光颜色")]
    public Color ambientColor = new Color(0.5f, 0.6f, 0.7f);
    [Tooltip("全局环境光强度")]
    public float ambientIntensity = 0.1f;
    
    //[Header("====== Light Settings ======")]
    [Header(">>Base Light Settings")]
    [Tooltip("云层消光系数,整体光线散射吸收系数")]
    [Range(0f, 10f)] public float extinction = 0.1f;
    [Tooltip("额外太阳光步进散射吸收系数")]
    [Range(0f, 15f)]public float absorption = 7f;
    [Tooltip("全局散射强度,体积强度")]
    [Range(0f, 10f)] public float scatterIntensity = 1.0f;

    [Header(">>HenyeyGreenstein Settings")]
    [Tooltip("光相位")]
    [Range(0f, 1f)] public float anisotropy = 0.6f;
    [Tooltip("银边差值")]
    [Range(0f, 1f)] public float silverSpread = 0.15f;
    [Range(0f, 100f)] public float silverIntensity = 1f;

    [Header(">>Multiple Scattering Settings")]
    [Tooltip("云层局部透光 - 深度系数")]
    [Range(0f, 10f)] public float multipleScatteringDepthPower = 0.2f;
    [Tooltip("云层局部透光 - 高度系数")]
    [Range(0f, 10f)] public float multipleScatteringHeightPower = 0.15f;
    [Tooltip("云层局部透光强度")]
    [Range(0f, 10f)] public float multipleScatterIntensity = 0.5f;

 
    [Header("===== 风场 =====")]
    [Header(">> weather map 风场")]
    public Vector2 weatherMoveDirection = Vector2.zero;
    public float weatherMoveSpeed = 1f;
    [Header(">> Cloud 风场")]
    public Vector3 cloudWindDirection = new Vector3(1, 0, 0);  // 云纹理风向
    public float cloudWindSpeed = 1f;                          // 云纹理风速


    
}

CloudRuntimeData

因为做天气系统需要传值,同时需要传值,所以我这里使用统一的一个WeatherSystem来进行数据管理,同时管理WeatherMapGenerator和对CloudFeature的参数传值。这里对需要更新的数据集放在一起。流程是WeatherSystem读取WeatherPreset参数,一部分参数先给到WeatherMapGenerator得到WeatherMap,然后连带其它参数和WeatherMap打包传给Feature

using UnityEngine;

public class CloudRuntimeData
{
    // WeatherMap RT
    public RenderTexture weatherMap;
    public float weatherMapScale;

    // 对流层云
    public float cloudBottom;
    public float cloudTop;
    public float shapeNoiseScale;
    public float detailNoiseScale;
    public float detailErodeType;
    public float detailErodeIntensity;
    public Color cloudColor;

    // 卷云
    public float cirrusBottom;
    public float cirrusScale;
    public float cirrusIntensity;
    public float cirrusAbsorption;
    public Color cirrusColor;

    // 地平线参数
    public float aerialDensity;   
    public Color horizonTint;
    // 风场
    public float cloudWindSpeed;
    public Vector3 cloudWindDirection;

    public float weatherMoveSpeed;
    public Vector2 weatherMoveDirection;
   
    // 光照
    public Color ambientColor;
    public float ambientIntensity;
    public float sunIntensity;

    public float ambientBaseIntensity;
    public float ambientFloor;
    public float ambientBottomStrength;
    public float powderEffectIntensity;

    // 渲染基础
    public float extinction;
    public float absorption;
    public float scatterIntensity;

    // HG
    public float anisotropy;
    public float silverSpread;
    public float silverIntensity;

    // Multiple Scattering
    public float msDepthPower;
    public float msHeightPower;
    public float msIntensity;
}

WeatherSystem

WeatherSystem负责天气切换,读取列表中的天气预设进行切换,并更新CloudRuntimeData

using UnityEngine;

[ExecuteAlways]
public class WeatherSystem : MonoBehaviour
{
    public static WeatherSystem Instance { get; private set; }

    [Header("References")]
    public WeatherMapGenerator weatherMapGenerator;

    [Header("Weather Presets")]
    public WeatherPreset[] presets;
    public int currentPresetIndex = 0;
    public int targetPresetIndex  = 0;

    [Header("Transition")]
    public float transitionDuration = 10f;
    private float transitionProgress = 1f;
    private WeatherPreset transitionFrom;

    // Feature 读取数据
    public CloudRuntimeData RuntimeData { get; private set; } = new CloudRuntimeData();

    void Awake()
    {
        Instance = this;
    }

    void Start()
    {
        if (presets != null && presets.Length > 0 && presets[currentPresetIndex] != null)
            ApplyPreset(presets[currentPresetIndex]);
    }

    void Update()
    {
        if (transitionProgress < 1f)
        {
            transitionProgress += Time.deltaTime / transitionDuration;
            transitionProgress = Mathf.Clamp01(transitionProgress);

            float t = Mathf.SmoothStep(0f, 1f, transitionProgress);
            BlendPresets(transitionFrom, presets[targetPresetIndex], t);

            if (transitionProgress >= 1f)
                currentPresetIndex = targetPresetIndex;
        }
        else
        {
            // 过渡完成后,每帧刷新当前预设(编辑器和运行时都生效)
            if (presets != null && presets.Length > 0 && presets[currentPresetIndex] != null)
                ApplyPreset(presets[currentPresetIndex]);
        }
        // 每帧更新 RT 引用(RT 可能被重新创建)
        if (weatherMapGenerator != null)
            RuntimeData.weatherMap = weatherMapGenerator.weatherMap;
    }

    public void TransitionToWeather(int presetIndex)
    {
        if (presetIndex == targetPresetIndex) return;
        if (presetIndex < 0 || presetIndex >= presets.Length) return;

        transitionFrom        = CreateCurrentSnapshot();
        targetPresetIndex     = presetIndex;
        transitionProgress    = 0f;
    }

    public void SetWeatherImmediate(int presetIndex)
    {
        if (presetIndex < 0 || presetIndex >= presets.Length) return;
        currentPresetIndex = presetIndex;
        targetPresetIndex  = presetIndex;
        transitionProgress = 1f;
        ApplyPreset(presets[presetIndex]);
    }

   void BlendPresets(WeatherPreset from, WeatherPreset to, float t)
    {
        if (from == null || to == null) return;

        // 对流层云
        RuntimeData.cloudBottom          = Mathf.Lerp(from.cloudBottom,          to.cloudBottom,          t);
        RuntimeData.cloudTop             = Mathf.Lerp(from.cloudTop,             to.cloudTop,             t);
        RuntimeData.shapeNoiseScale      = Mathf.Lerp(from.shapeNoiseScale,      to.shapeNoiseScale,      t);
        RuntimeData.detailNoiseScale     = Mathf.Lerp(from.detailNoiseScale,     to.detailNoiseScale,     t);
        RuntimeData.detailErodeType      = Mathf.Lerp(from.detailErodeType,      to.detailErodeType,      t);
        RuntimeData.detailErodeIntensity = Mathf.Lerp(from.detailErodeIntensity, to.detailErodeIntensity, t);
        RuntimeData.cloudColor           = Color.Lerp(from.cloudColor,           to.cloudColor,           t);
 
        // 卷云
        RuntimeData.cirrusBottom    = Mathf.Lerp(from.cirrusBottom,    to.cirrusBottom,    t);
        RuntimeData.cirrusScale     = Mathf.Lerp(from.cirrusScale,     to.cirrusScale,     t);
        RuntimeData.cirrusIntensity = Mathf.Lerp(from.cirrusIntensity, to.cirrusIntensity, t);
        RuntimeData.cirrusAbsorption= Mathf.Lerp(from.cirrusAbsorption,to.cirrusAbsorption,t);
        RuntimeData.cirrusColor     = Color.Lerp(from.cirrusColor,     to.cirrusColor,     t);

        // 地平线参数
        RuntimeData.aerialDensity   = Mathf.Lerp(from.aerialDensity, to.aerialDensity, t);
        RuntimeData.horizonTint     = Color.Lerp(from.horizonTint,   to.horizonTint,   t);

        // 风场
        RuntimeData.cloudWindSpeed     = Mathf.Lerp(from.cloudWindSpeed,      to.cloudWindSpeed,     t);
        RuntimeData.cloudWindDirection = Vector3.Lerp(from.cloudWindDirection,to.cloudWindDirection, t).normalized;

        RuntimeData.weatherMoveSpeed     = Mathf.Lerp(from.weatherMoveSpeed,     to.weatherMoveSpeed,     t);
        RuntimeData.weatherMoveDirection = Vector2.Lerp(from.weatherMoveDirection, to.weatherMoveDirection, t).normalized;
        // 光照
        RuntimeData.ambientColor          = Color.Lerp(from.ambientColor,          to.ambientColor,          t);
        RuntimeData.ambientIntensity      = Mathf.Lerp(from.ambientIntensity,      to.ambientIntensity,      t);
        RuntimeData.sunIntensity          = Mathf.Lerp(from.sunIntensity,          to.sunIntensity,          t);
        RuntimeData.ambientBaseIntensity  = Mathf.Lerp(from.ambientBaseIntensity,  to.ambientBaseIntensity,  t);
        RuntimeData.ambientFloor          = Mathf.Lerp(from.ambientFloor,          to.ambientFloor,          t);
        RuntimeData.ambientBottomStrength = Mathf.Lerp(from.ambientBottomStrength, to.ambientBottomStrength, t);
        RuntimeData.powderEffectIntensity = Mathf.Lerp(from.powderEffectIntensity, to.powderEffectIntensity, t);
        
 
        // 渲染基础
        RuntimeData.extinction       = Mathf.Lerp(from.extinction,       to.extinction,       t);
        RuntimeData.absorption       = Mathf.Lerp(from.absorption,       to.absorption,       t);
        RuntimeData.scatterIntensity = Mathf.Lerp(from.scatterIntensity, to.scatterIntensity, t);

        // HG
        RuntimeData.anisotropy       = Mathf.Lerp(from.anisotropy,       to.anisotropy,       t);
        RuntimeData.silverSpread     = Mathf.Lerp(from.silverSpread,     to.silverSpread,     t);
        RuntimeData.silverIntensity  = Mathf.Lerp(from.silverIntensity,  to.silverIntensity,  t);

        // Multiple Scattering
        RuntimeData.msDepthPower  = Mathf.Lerp(from.multipleScatteringDepthPower,  to.multipleScatteringDepthPower,  t);
        RuntimeData.msHeightPower = Mathf.Lerp(from.multipleScatteringHeightPower, to.multipleScatteringHeightPower, t);
        RuntimeData.msIntensity   = Mathf.Lerp(from.multipleScatterIntensity,      to.multipleScatterIntensity,      t);

        // WeatherMap RT 引用
        RuntimeData.weatherMap      = weatherMapGenerator?.weatherMap;
        RuntimeData.weatherMapScale = weatherMapGenerator?.weatherMapScale ?? 16000f;

        // 驱动 WeatherMapGenerator
        if (weatherMapGenerator != null)
        {
            weatherMapGenerator.coverageBias        = Mathf.Lerp(from.coverageBias,       to.coverageBias,       t);
            weatherMapGenerator.coverageScale       = Mathf.Lerp(from.coverageScale,      to.coverageScale,      t);
            weatherMapGenerator.cloudTypeBias       = Mathf.Lerp(from.cloudTypeBias,      to.cloudTypeBias,      t);
            weatherMapGenerator.cloudTypeScale      = Mathf.Lerp(from.cloudTypeScale,     to.cloudTypeScale,     t);
            weatherMapGenerator.cirrusCoverageBias  = Mathf.Lerp(from.cirrusCoverageBias, to.cirrusCoverageBias, t);
            weatherMapGenerator.cirrusCoverageScale = Mathf.Lerp(from.cirrusCoverageScale,to.cirrusCoverageScale,t);
            weatherMapGenerator.cirrusTypeBias      = Mathf.Lerp(from.cirrusTypeBias,     to.cirrusTypeBias,     t);
            weatherMapGenerator.cirrusTypeScale     = Mathf.Lerp(from.cirrusTypeScale,    to.cirrusTypeScale,    t);
        }

        // 驱动太阳光
        Light sun = RenderSettings.sun;
        if (sun != null)
            sun.intensity = RuntimeData.sunIntensity;
    }

    void ApplyPreset(WeatherPreset preset)
    {
        BlendPresets(preset, preset, 1f);
    }

    WeatherPreset CreateCurrentSnapshot()
    {
        var snap = ScriptableObject.CreateInstance<WeatherPreset>();

        // 从 RuntimeData 读取当前状态
        
        snap.cloudBottom                 = RuntimeData.cloudBottom;
        snap.cloudTop                    = RuntimeData.cloudTop;
        snap.cirrusBottom                = RuntimeData.cirrusBottom;
        snap.cirrusScale                 = RuntimeData.cirrusScale;
        snap.cirrusIntensity             = RuntimeData.cirrusIntensity;
        snap.cirrusAbsorption            = RuntimeData.cirrusAbsorption;
        snap.shapeNoiseScale             = RuntimeData.shapeNoiseScale;
        snap.detailNoiseScale            = RuntimeData.detailNoiseScale;
        snap.detailErodeType             = RuntimeData.detailErodeType;
        snap.detailErodeIntensity        = RuntimeData.detailErodeIntensity;
        snap.cirrusColor                 = RuntimeData.cirrusColor;
        snap.cloudWindSpeed              = RuntimeData.cloudWindSpeed;
        snap.cloudWindDirection          = RuntimeData.cloudWindDirection;
        snap.weatherMoveSpeed            = RuntimeData.weatherMoveSpeed;
        snap.weatherMoveDirection        = RuntimeData.weatherMoveDirection;
        snap.ambientColor                = RuntimeData.ambientColor;
        snap.ambientIntensity            = RuntimeData.ambientIntensity;
        snap.ambientBaseIntensity        = RuntimeData.ambientBaseIntensity;
        snap.ambientFloor                = RuntimeData.ambientFloor;
        snap.ambientBottomStrength       = RuntimeData.ambientBottomStrength;  
        snap.powderEffectIntensity       = RuntimeData.powderEffectIntensity;  
        snap.sunIntensity                = RuntimeData.sunIntensity;
        snap.cloudColor                  = RuntimeData.cloudColor;
        snap.aerialDensity               = RuntimeData.aerialDensity;
        snap.horizonTint                 = RuntimeData.horizonTint;
        snap.extinction                  = RuntimeData.extinction;
        snap.absorption                  = RuntimeData.absorption;
        snap.scatterIntensity            = RuntimeData.scatterIntensity;
        snap.anisotropy                  = RuntimeData.anisotropy;
        snap.silverSpread                = RuntimeData.silverSpread;
        snap.silverIntensity             = RuntimeData.silverIntensity;
        snap.multipleScatteringDepthPower   = RuntimeData.msDepthPower;
        snap.multipleScatteringHeightPower  = RuntimeData.msHeightPower;
        snap.multipleScatterIntensity       = RuntimeData.msIntensity;
      

        // WeatherMapGenerator 参数
        if (weatherMapGenerator != null)
        {
            snap.coverageBias            = weatherMapGenerator.coverageBias;
            snap.coverageScale           = weatherMapGenerator.coverageScale;
            snap.cloudTypeBias           = weatherMapGenerator.cloudTypeBias;   
            snap.cloudTypeScale          = weatherMapGenerator.cloudTypeScale;
            snap.cirrusCoverageBias      = weatherMapGenerator.cirrusCoverageBias;
            snap.cirrusCoverageScale     = weatherMapGenerator.cirrusCoverageScale; 
            snap.cirrusTypeBias          = weatherMapGenerator.cirrusTypeBias;   
            snap.cirrusTypeScale         = weatherMapGenerator.cirrusTypeScale;  
        }

        return snap;
    }

    void OnDestroy()
    {
        if (transitionFrom != null)
        {
#if UNITY_EDITOR
            if (!UnityEditor.AssetDatabase.Contains(transitionFrom))
                Destroy(transitionFrom);
#else
            Destroy(transitionFrom);
#endif
        }
    }

    void OnGUI()
    {
        if (!Application.isPlaying) return;

        float scale = Screen.height / 1080f;
        GUI.matrix = Matrix4x4.TRS(Vector3.zero, Quaternion.identity, 
                                    new Vector3(scale, scale, 1));

        GUILayout.BeginArea(new Rect(10, 100, 250, 300));
        GUI.backgroundColor = Color.black;
        GUILayout.Label("=== Weather System ===");

        if (presets != null)
        {
            for (int i = 0; i < presets.Length; i++)
            {
                if (presets[i] == null) continue;
                GUI.backgroundColor = (i == currentPresetIndex) ? Color.green : Color.white;
                if (GUILayout.Button(presets[i].name))
                    TransitionToWeather(i);
            }
        }

        GUI.backgroundColor = Color.white;
        GUILayout.Label($"Transition: {transitionProgress * 100:F0}%");
        GUILayout.EndArea();

        GUI.matrix = Matrix4x4.identity;
    }
}

切换效果

Feature里面读取CloudRuntimeData就行,默认使用settings进行回退

大致切换效果如下,这里设置的过渡时间很短

WeatherMapPainter

因为有需要手动修改WeatherMap的情况,所以添加了绘制工具

WeatherMapPainterWindow

工具脚本

#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;

public class WeatherMapPainterWindow : EditorWindow
{
    RenderTexture canvas;            // 单通道编辑画布,灰度图,只反映当前选中的那一个通道
    Material brushMaterial;          // 落笔用,Hidden/WeatherBrush
    Material channelExtractMat;      // 读入用,Hidden/ChannelExtract,从weatherMap抽取单通道铺到canvas
    Material channelWriteBackMat;    // 写出用,Hidden/ChannelWriteBack,只把canvas写回weatherMap对应通道

    public Vector4 brushColor = new Vector4(1, 1, 1, 1); // 灰度画布上直接用亮度值,取c.r即可,四分量给1方便lerp
    public float brushSize = 0.05f;
    public float brushHardness = 0.5f;
    public bool eraseMode = false;

    // 当前正在编辑哪个通道:0=Coverage(R) 1=CloudType(G) 2=CirrusCoverage(B) 3=CirrusType(A)
    public int editChannel = 0;
    static readonly string[] channelNames = { "Coverage (R)", "CloudType (G)", "CirrusCoverage (B)", "CirrusType (A)" };
    static readonly Vector4[] channelSelectVectors = {
        new Vector4(1,0,0,0), new Vector4(0,1,0,0), new Vector4(0,0,1,0), new Vector4(0,0,0,1)
    };
    static readonly int[] channelColorMaskBits = { 8, 4, 2, 1 }; // ShaderLab ColorMask 位掩码:R=8 G=4 B=2 A=1

    [MenuItem("Tools/Weather Map Painter")]
    static void OpenWindow()
    {
        var window = GetWindow<WeatherMapPainterWindow>("Weather Painter");
        window.minSize = new Vector2(300, 600); // 给一个合理的最小尺寸
    }

    void OnEnable()
    {
        if (canvas == null)
        {
            canvas = new RenderTexture(512, 512, 0, RenderTextureFormat.ARGB32);
            canvas.Create();
        }
        if (brushMaterial == null)
            brushMaterial = new Material(Shader.Find("Hidden/WeatherBrush"));
        if (channelExtractMat == null)
            channelExtractMat = new Material(Shader.Find("Hidden/ChannelExtract"));
        if (channelWriteBackMat == null)
            channelWriteBackMat = new Material(Shader.Find("Hidden/ChannelWriteBack"));
    }

    void OnGUI()
    {
        EditorGUILayout.LabelField("正在编辑的通道", EditorStyles.boldLabel);
        editChannel = EditorGUILayout.Popup(editChannel, channelNames);

        EditorGUILayout.Space(6);
        if (GUILayout.Button("从 Procedural 读入该通道"))
            InitFromProcedural();
        if (GUILayout.Button("清空该通道为纯色(完全手绘模式)"))
            InitBlank();

        EditorGUILayout.Space(6);
        if (GUILayout.Button("应用到场景(只写回当前通道)"))
            ApplyToScene();
        if (GUILayout.Button("恢复程序化生成(放弃手绘)"))
            RestoreProcedural();

        EditorGUILayout.Space(6);
        brushSize = EditorGUILayout.Slider("笔刷大小", brushSize, 0.005f, 0.3f);
        brushHardness = EditorGUILayout.Slider("笔刷软硬度", brushHardness, 0.0f, 1.0f);
        float brushValue = EditorGUILayout.Slider("笔刷强度(该通道目标值)", brushColor.x, 0f, 1f);
        brushColor = new Vector4(brushValue, brushValue, brushValue, 1f);
        eraseMode = EditorGUILayout.Toggle("擦除模式(画为0)", eraseMode);

        EditorGUILayout.Space(6);
        //var rect = GUILayoutUtility.GetRect(512, 512);

        // float canvasSize = Mathf.Min(position.width - 20, 512); // 不超过窗口宽度,但最大512
        // var rect = GUILayoutUtility.GetRect(canvasSize, canvasSize, GUILayout.Width(canvasSize), GUILayout.Height(canvasSize));


        float canvasSize = Mathf.Min(position.width - 20, 512);
        EditorGUILayout.BeginHorizontal();
        GUILayout.FlexibleSpace();   // 左侧弹性空间,把画布推向中间
        var rect = GUILayoutUtility.GetRect(canvasSize, canvasSize, GUILayout.Width(canvasSize), GUILayout.Height(canvasSize));
        DrawCanvasAndHandleInput(rect);
        GUILayout.FlexibleSpace();   // 右侧弹性空间,两边对称
        EditorGUILayout.EndHorizontal();

        DrawCanvasAndHandleInput(rect);
    }

    // ---- 读入:从 weatherMap 抽取当前选中的单通道,铺满到 canvas(灰度) ----
    void InitFromProcedural()
    {
        var gen = WeatherMapGenerator.Instance;
        if (gen == null || gen.weatherMap == null) return;

        channelExtractMat.SetVector("_ChannelSelect", channelSelectVectors[editChannel]);
        Graphics.Blit(gen.weatherMap, canvas, channelExtractMat);
    }

    void InitBlank()
    {
        RenderTexture.active = canvas;
        GL.Clear(true, true, new Color(0.5f, 0.5f, 0.5f, 1f)); // 灰度画布,清成中灰
        RenderTexture.active = null;
    }

    // ---- 写出:只把 canvas 的灰度值写回 weatherMap 对应的那一个通道,其余通道不受影响 ----
    void ApplyToScene()
    {
        var gen = WeatherMapGenerator.Instance;
        if (gen == null || gen.weatherMap == null) return;

        gen.isPainterOverride = true;
        channelWriteBackMat.SetInt("_ColorMask", channelColorMaskBits[editChannel]);
        Graphics.Blit(canvas, gen.weatherMap, channelWriteBackMat);
        gen.weatherMap.GenerateMips();
    }

    void RestoreProcedural()
    {
        var gen = WeatherMapGenerator.Instance;
        if (gen == null) return;

        gen.isPainterOverride = false;
        gen.Generate();
        gen.weatherMap.GenerateMips();
    }

    void DrawCanvasAndHandleInput(Rect rect)
    {
        EditorGUI.DrawPreviewTexture(rect, canvas);

        Event e = Event.current;
        if (e.type == EventType.MouseDrag && rect.Contains(e.mousePosition))
        {
            Vector2 uv = (e.mousePosition - rect.position) / rect.size;
            uv.y = 1 - uv.y;
            PaintAt(uv);
            Repaint();
        }
    }

    void PaintAt(Vector2 uv)
    {
        Vector4 targetColor = eraseMode ? new Vector4(0f, 0f, 0f, 1f) : brushColor;

        brushMaterial.SetVector("_BrushColor", targetColor);
        brushMaterial.SetVector("_ChannelMask", new Vector4(1, 1, 1, 1)); // 灰度画布本身不分通道,恒全开
        brushMaterial.SetVector("_BrushUV", uv);
        brushMaterial.SetFloat("_BrushSize", brushSize);
        brushMaterial.SetFloat("_BrushHardness", brushHardness);

        var temp = RenderTexture.GetTemporary(canvas.width, canvas.height, 0, canvas.format);
        Graphics.Blit(canvas, temp, brushMaterial);
        Graphics.Blit(temp, canvas);
        RenderTexture.ReleaseTemporary(temp);
    }
}
#endif

WeatherBrush

Shader "Hidden/WeatherBrush"
{
    Properties { _MainTex ("Base", 2D) = "white" {} }
    SubShader
    {
        Tags { "RenderPipeline" = "UniversalPipeline" }
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);
            float4 _BrushColor;
            float4 _ChannelMask;   // 画布是灰度单通道编辑模式下,这里恒为(1,1,1,1)即可,留作以后多通道混合画布备用
            float2 _BrushUV;
            float _BrushSize;
            float _BrushHardness;

            struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; };
            struct Varyings   { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; };

            Varyings vert(Attributes v)
            {
                Varyings o;
                o.positionCS = TransformObjectToHClip(v.positionOS.xyz);
                o.uv = v.uv;
                return o;
            }

            float4 frag(Varyings i) : SV_Target
            {
                float4 baseColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
                float dist = length(i.uv - _BrushUV);
                float mask = 1.0 - smoothstep(_BrushSize * _BrushHardness, _BrushSize, dist);
                float4 painted = lerp(baseColor, _BrushColor, mask);
                return lerp(baseColor, painted, _ChannelMask);
            }
            ENDHLSL
        }
    }
}

ChannelWriteBack

Shader "Hidden/ChannelWriteBack"
{
    // 写出用:把灰度画布的值,只写回目标通道,其余通道由ColorMask保护不受影响
    Properties
    {
        _MainTex ("Canvas", 2D) = "white" {}
        _ColorMask ("Color Mask", Int) = 8   // 位掩码 R=8 G=4 B=2 A=1
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        ColorMask [_ColorMask]

        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            sampler2D _MainTex;

            struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; };
            struct v2f     { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; };

            v2f vert(appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                float v = tex2D(_MainTex, i.uv).r;   // canvas 是灰度,R即代表该值
                return fixed4(v, v, v, v);           // 四分量都填v,实际写入哪个由ColorMask决定
            }
            ENDHLSL
        }
    }
}

ChannelExtract

Shader "Hidden/ChannelExtract"
{
    // 读入用:从多通道纹理里抽取目标通道,复制到RGB三通道,显示为灰度
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _ChannelSelect ("Channel Select", Vector) = (1,0,0,0)
    }
    SubShader
    {
        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            sampler2D _MainTex;
            float4 _ChannelSelect;

            struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; };
            struct v2f     { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; };

            v2f vert(appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                fixed4 c = tex2D(_MainTex, i.uv);
                float v = dot(c, _ChannelSelect);
                return fixed4(v, v, v, 1.0);
            }
            ENDHLSL
        }
    }
}

Cloud Shadow

ShadowMap

computerShader

使用computerShader拿到weatherMap从太阳方向做一次步进来计算得到ShadowMap

#pragma kernel GenerateCloudShadowMap

RWTexture2D<float> _CloudShadowMap;   // 单通道,存transmittance(0=完全遮挡, 1=完全透光)

Texture2D<float4> _WeatherMap;
SamplerState sampler_WeatherMap;
Texture3D<float4> _ShapeNoiseTex;
SamplerState sampler_ShapeNoiseTex;
Texture2D<float> _HeightProfileLUT;
SamplerState sampler_HeightProfileLUT;

int _ShadowMapResolution;
float _WeatherMapScale;
float _CloudBottom;
float _CloudTop;
float _Coverage;
float _Absorption;
float _ShapeNoiseScale;
float3 _SunDirection;       // 太阳方向(光线传播方向,从太阳射向地面)
float3 _WindDirection;
float _WindSpeed;
float _CurrentTime;
float3 _WeatherMoveDirection;
float _WeatherMoveSpeed;

float remap(float v, float oldMin, float oldMax, float newMin, float newMax)
{
    return newMin + (v - oldMin) / (oldMax - oldMin) * (newMax - newMin);
}

float SampleCloudProfile(float heightFraction, float cloudType)
{
    float2 uv = float2(cloudType, heightFraction);
    return saturate(_HeightProfileLUT.SampleLevel(sampler_HeightProfileLUT, uv, 0));
}

[numthreads(8,8,1)]
void GenerateCloudShadowMap(uint3 id : SV_DispatchThreadID)
{
    float2 uv01 = (id.xy + 0.5) / (float)_ShadowMapResolution;
    float2 worldXZ = (uv01 - 0.5) * _WeatherMapScale;

    float3 rayDir = normalize(-_SunDirection); // 从地面反向朝太阳走
    float3 rayStart = float3(worldXZ.x, _CloudBottom, worldXZ.y);

    float layerThickness = _CloudTop - _CloudBottom;
    float travelDistPerUnitHeight = layerThickness / max(rayDir.y, 0.15); // 太阳低角度时的保护

    static const float stepFractions[6] = { 0.05, 0.15, 0.3, 0.5, 0.7, 0.9 };
    static const float stepWeights[6]   = { 1, 1, 2, 2, 3, 3 };

    float totalDensity = 0;
    float3 windOffset = _WindDirection * _CurrentTime * _WindSpeed;
    float2 weatherOffset = _WeatherMoveDirection.xz * _WeatherMoveSpeed * _CurrentTime;

    [unroll]
    for (int i = 0; i < 6; i++)
    {
        float t = stepFractions[i] * travelDistPerUnitHeight;
        float3 p = rayStart + rayDir * t;

        float heightFraction = saturate((p.y - _CloudBottom) / layerThickness);

        float2 wUV = p.xz / _WeatherMapScale + 0.5 + weatherOffset;
        float4 w = _WeatherMap.SampleLevel(sampler_WeatherMap, wUV, 2);
        float coverage = w.r * _Coverage;
        float cloudType = w.g;

        float profile = SampleCloudProfile(heightFraction, cloudType) * coverage;

        float3 animatedPos = p + windOffset;
        float4 shape = _ShapeNoiseTex.SampleLevel(sampler_ShapeNoiseTex, animatedPos * _ShapeNoiseScale, 1);
        float worleyFBM = shape.g * 0.625 + shape.b * 0.25 + shape.a * 0.125;
        float baseCloud = saturate(remap(shape.r, worleyFBM - 1.0, 1.0, 0.0, 1.0));
        float density = saturate(baseCloud - (1.0 - profile));

        totalDensity += density * stepWeights[i];
    }

    float transmittance = exp(-totalDensity * _Absorption * 0.15); // 归一化系数,跑起来后按效果调
    _CloudShadowMap[id.xy] = saturate(transmittance);
}

C#相关代码

参数

ComputerShader传值

调用生成,进行Blit

ShadowPass

Pass
{
	Name "ApplyGroundShadow"
	HLSLPROGRAM
	#pragma vertex vert
	#pragma fragment frag

	float4 frag(Varyings input) : SV_Target
	{
		float4 sceneColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv);

		float rawDepth = SampleSceneDepth(input.uv);
		bool isSky = false;
		#if defined(UNITY_REVERSED_Z)
			if (rawDepth <= 0.00001) isSky = true;
		#else
			if (rawDepth >= 0.99999) isSky = true;
		#endif
		if (isSky) return sceneColor; // 天空(云自己)不处理

		// 重建世界坐标
		float4 ndc = float4(input.uv * 2.0 - 1.0, rawDepth, 1.0);
		float4 worldPos = mul(_InverseVP, ndc);
		worldPos /= worldPos.w;

		// 用世界坐标(x,z)采样Shadow Map,UV空间和ShadowMap生成时保持一致
		float2 shadowUV = worldPos.xz  / _WeatherMapScale * 100 + 0.5;
		float shadowValue = SAMPLE_TEXTURE2D(_CloudShadowMap, sampler_CloudShadowMap, shadowUV).r;

		//sceneColor.rgb *= lerp(1.0, shadowValue, _CloudShadowStrength);
		sceneColor.rgb *= lerp(1.0, pow(shadowValue, 2),1);
		//return float4(shadowValue.xxx,1);
		return sceneColor;
	}
	ENDHLSL
}

缺陷

最开始使用间隔刷新,但是因为云的尺度比较大,加上风速,这样会导致跳变,这里虽然看起来是对齐的,实际上缩小了100倍,真正正确的阴影会覆盖整片区域了,因为尺度确实太大了

如果换成每帧刷新就很顺滑了,不过这样的流程会让ShaderMap生成频率更快,如果直接在Shader中处理就可以省掉生成这一步,同时也能减少传值

ShdowMap的简化

直接增加Pass,不在进行烘焙ShdowMap,使用少数几次采样来模拟阴影,不需要真的从太阳方向进行步进,也能减少传值

Pass
{
	Name "ApplyGroundShadow"
	HLSLPROGRAM
	#pragma vertex vert
	#pragma fragment frag
	float _CloudShadowScale;
	
	float4 frag(Varyings input) : SV_Target
	{
		float4 sceneColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv);
		float rawDepth = SampleSceneDepth(input.uv);

		// 天空(云自己)不处理,只压暗有真实深度的物体
		bool isSky = false;
		#if defined(UNITY_REVERSED_Z)
			if (rawDepth <= 0.00001) isSky = true;
		#else
			if (rawDepth >= 0.99999) isSky = true;
		#endif
		if (isSky) return sceneColor;

		// 重建世界坐标
		float4 ndc = float4(input.uv * 2.0 - 1.0, rawDepth, 1.0);
		float4 worldPos = mul(_InverseVP, ndc);
		worldPos /= worldPos.w;

		Light mainLight = GetMainLight();
		float3 sunDir = mainLight.direction;

		float layerThickness = _CloudTop - _CloudBottom;
		float2 weatherOffset = _WeatherMoveDirection.xz * _WeatherMoveSpeed * _Time.y;
		float3 windOffset = _WindDirection * _Time.y * _WindSpeed;

		// ===== 3层高度采样,简化路径积分 =====
		const int sampleCount = 3;
		float totalCoverage = 0;
		float totalShapeMod = 0;

		[unroll]
		for (int i = 0; i < sampleCount; i++)
		{
			float heightFraction = (i + 0.5) / (float)sampleCount; // 0.167, 0.5, 0.833 三层
			float sampleHeight = _CloudBottom + heightFraction * layerThickness;
			float heightToSample = sampleHeight - worldPos.y;

			// 沿太阳方向的水平偏移
			float2 offset = sunDir.xz * (heightToSample / max(abs(sunDir.y), 0.15));
			float2 sampleXZ = worldPos.xz + offset;

			// weatherMap coverage
			float2 weatherUV = sampleXZ / _WeatherMapScale * _CloudShadowScale + 0.5 + weatherOffset;
			float coverage = SAMPLE_TEXTURE2D_LOD(_WeatherMap, sampler_WeatherMap, weatherUV, 0).r;
			totalCoverage += coverage;

			// ShapeNoise细节调制(粗mip,单次采样)
			float3 samplePos3D = float3(sampleXZ.x, sampleHeight, sampleXZ.y) + windOffset;
			float4 shape = SAMPLE_TEXTURE3D_LOD(_ShapeNoiseTex, sampler_ShapeNoiseTex, samplePos3D * _ShapeNoiseScale, 2);
			float worleyFBM = shape.g * 0.625 + shape.b * 0.25 + shape.a * 0.125;
			float baseCloud = saturate(worleyFBM * 0.5 + shape.r * 0.5);
			totalShapeMod += baseCloud;
		}

		totalCoverage /= sampleCount;
		totalShapeMod /= sampleCount;

		// ShapeNoise在coverage基础上做浮动,增加细节层次和风动感
		float density = totalCoverage * lerp(0.7, 1.3, totalShapeMod);

		float shadowFactor = lerp(1.0, 1.0 - _CloudShadowStrength, saturate(density));
		sceneColor.rgb *= shadowFactor;

		return sceneColor;
	}
	ENDHLSL
}

GodRays

为了让阳光透过云时更好看一点,添加了上帝光,值得一提的是这里没有使用光线追踪,因为云本身不是mesh,光线追踪根本不能识别到,不过通过读云的透射率或者说亮度,可以得到一个遮罩,然后取太阳光方向的一个点,进行径向模糊就可以得到类似效果,这样还能正确得到云的轮廓。效果反正我觉得挺满意的,代码放在最后完整代码中

CustomVolumetricCloud完整代码

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class CustomVolumetricCloud : ScriptableRendererFeature
{
    
    [System.Serializable]
    public class CustomVolumetricCloudSettings
    {
        public Shader customVolumetricCloudShader; //Shader指定
        //========== Texture Setting ==============
        [Header("====== Global Texture Settings ======")]
        [Tooltip("cloud Shape Noise")]
        public Texture3D cloudShapeNoiseTexture;
        [Range(0f, 100f)]public float shapeNoiseScale = 1f;

        [Tooltip("cloud Detail Noise")]
        public Texture3D cloudDetailNoiseTexture;
        [Range(0f, 100f)]public float detailNoiseScale = 1f;

        [Tooltip("卷云 Texture")]
        public Texture2D cirrusNoiseTexture;
        [Tooltip("卷云采样缩放")]
        [Range(0f, 0.001f)] public float cirrusScale = 0.00005f;   

        [Header("====== Cloud Shadow Settings ======")]
        public bool enablecloudShadow = false;
        [Tooltip("阴影采样缩放,对齐云是1,根据视角可临时调整")]
        public float cloudShadowScale = 1f;
        [Tooltip("阴影强度")]
        public float cloudShadowStrength = 0.4f;

        [Header("====== God Rays Settings ======")]
        public bool enableGodRays = false;
        [Tooltip("光线长度")]
        [Range(0f, 3f)] public float godRayDensity = 1.0f;      
        [Tooltip("光线衰减")]
        [Range(0.8f, 0.995f)] public float godRayDecay = 0.96f; 
        [Tooltip("光线每步采样贡献的权重")]
        [Range(0f, 2f)] public float godRayWeight = 0.5f;       
        [Tooltip("光线整体强度")]
        [Range(0f, 5f)] public float godRayIntensity = 1.0f;    
        [Tooltip("采样步数")]
        [Range(8, 128)] public int godRaySampleCount = 64;      
        [Tooltip("非天空物体(场景物体)贡献权重")]
        [Range(0f, 1f)] public float godRayObjectContribution = 1.0f;    
    


        //========== Cloud Setting ================
        [Header("====== Cloud Base Settings ======")]
        [Header(">> Stratus Cloud Settings")]
        [Tooltip("云层底部高度")]
        public float cloudBottom = 400f;
        [Tooltip("云层顶部高度")]
        public float cloudTop = 1500f;

        [Tooltip("垂直云基础类型顶部切换:层云-层积云-积云")]
        [Range(0f, 1f)] public float topType = 0.5f;  

        [Tooltip("垂直云基础类型底部切换:层云-层积云-积云")]
        [Range(0f, 1f)] public float bottomType = 0.5f;  

        [Tooltip("云层基础密度阈值")]
        [Range(0f, 10f)] public float densityThreshold = 0.7f;

        [Tooltip("cloud Detail Noise Erode Type(侵蚀样式)")]
        [Range(0f, 1f)]public float detailErodeType = 1f;

        [Tooltip("cloud Detail Noise Erode Intensity(侵蚀强度)")]
        [Range(0f, 5f)]public float detailErodeIntensity = 1f;


        [Tooltip("层-积云颜色")]
        public Color cloudColor = Color.white;
        [Tooltip("Stratus云层覆盖系数")]
        [Range(0f, 1f)] public float coverage = 0.5f;

        [Header(">> Cirrus Cloud Settings")]
        
        [Tooltip("卷云高度")]
        [Range(0f, 10000f)] public float cirrusBottom = 6000f; 

        [Tooltip("卷云亮度")]
        [Range(0f, 2f)] public float cirrusIntensity = 0.3f;     

        [Tooltip("卷云类型:条纹状-丝缕状-团状")]
        [Range(0f, 1f)]public float cirrusType = 0f;

        [Tooltip("cirrus云层覆盖系数")]
        [Range(0f, 1f)] public float cirrusCoverage = 0.5f;

        [Tooltip("cirrus额外太阳光步进散射吸收系数")]
        [Range(0f, 10f)] public float cirrusAbsorption = 1f;
        [Tooltip("卷云颜色")]
        public Color cirrusColor = Color.white;

        [Header(">> Horizon cloud Fade Settings")]
        [Tooltip("地平线云密度")]
        [Range(0f, 0.1f)] public float aerialDensity = 0.00005f;
        [Tooltip("地平线云颜色(使用天空颜色较好)")]
        public Color horizonTint = Color.white;
     
        [Header("====== Light Settings ======")]
        [Header(">>Base Light Settings")]
        [Tooltip("云层消光系数,整体光线散射吸收系数")]
        [Range(0f, 10f)] public float extinction = 0.1f;

        [Tooltip("额外太阳光步进散射吸收系数")]
        [Range(-10f, 10f)]public float absorption = 1f;

        [Tooltip("全局散射强度,体积强度")]
        [Range(0f, 10f)] public float scatterIntensity = 1.0f;

        [Header(">>HenyeyGreenstein Settings")]
        [Tooltip("光相位")]
        [Range(0f, 1f)] public float anisotropy = 0.5f;

        [Tooltip("银边差值")]
        [Range(0f, 1f)] public float silverSpread = 1f;
        [Range(0f, 100f)] public float silverIntensity = 1f;

        [Header(">>Multiple Scattering Settings")]
        [Tooltip("云层局部透光 - 深度系数")]
        [Range(0f, 10f)] public float multipleScatteringDepthPower = 1f;
        [Tooltip("云层局部透光 - 高度系数")]
        [Range(0f, 10f)] public float multipleScatteringHeightPower = 1f;
        [Tooltip("云层局部透光强度")]
        [Range(0f, 10f)] public float multipleScatterIntensity = 0.5f;

        
        [Header("====== Ambient Settings ======")]
        [Tooltip("环境光最低基本限度强度")]
        [Range(0f, 1f)] public float ambientBaseIntensity = 0.1f;
        [Tooltip("云体环境光起始位置")]
        [Range(0f, 1f)] public float ambientFloor = 0.0f;
        [Tooltip("底部环境光强度")]
        [Range(0f, 1f)] public float ambientBottomStrength = 0.2f;
        [Tooltip("糖粉效果强度")]
        [Range(0f, 5f)] public float powderEffectIntensity = 0.1f;

        [Tooltip("环境光颜色")]
        [ColorUsage(false, false)] public Color ambientColor = new Color(0.1f, 0.15f, 0.2f);
        
        [Tooltip("全局环境光强度")]
        [Range(0f, 1f)] public float ambientIntensity = 1.0f;

        //========== wind Setting ================
        [Header("====== Wind Settings ======")]
        [Tooltip("windDirection")]
        public Vector3 windDirection = new Vector3(1, 0, 0);
        public float windSpeed = 100f;
        [Tooltip("weatherMoveDirection")]
        public Vector3 weatherMoveDirection = new Vector3(1, 0, 0);
        public float weatherMoveSpeed = 1f;
        [Header("云类型")]
        [Range(0f, 1f)] 
        public float cloudType = 0.5f;  // 0=层云 0.5=积云 1=积雨云

        
        [Header("====== Curl Settings ======")]
        [Header("Curl Noise")]
        public Texture2D curlNoiseTexture;
        [Range(0f, 0.001f)] 
        public float curlScale = 0.00005f;        // Curl 采样缩放
        [Range(0f, 500f)] 
        public float curlStrength = 100f;         // Curl 扰动强度

        

        [Header("====== Quality Settings ======")]
        [Tooltip("光线步进Jitter随机偏移")]
        public bool useStepJitter = false;

        [Tooltip("光线步进Jitter蓝噪声纹理")]
        public Texture2D blueNoiseTexture;

        [Tooltip("蓝噪声纹理平铺倍数缩放")]
        [Range(0f, 10f)] public float blueNoiseTileScale = 1f;

        [Range(0f, 1f)]
        public float noiseStrength = 0.5f;
    }
    public CustomVolumetricCloudSettings settings = new CustomVolumetricCloudSettings(); 
    
    private CustomVolumetricCloudRenderPass pass;
    private Material material;


    /// <inheritdoc/>
    public override void Create()
    {
        if (settings.customVolumetricCloudShader == null) return;
        if (material == null)
        {
            material = CoreUtils.CreateEngineMaterial(settings.customVolumetricCloudShader);
        }
        pass = new CustomVolumetricCloudRenderPass(settings, material);

        // Configures where the render pass should be injected.
        pass.renderPassEvent = RenderPassEvent.AfterRenderingTransparents;
    }

   
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {   
        //if (renderingData.cameraData.cameraType != CameraType.Game) return;
        if (renderingData.cameraData.cameraType != CameraType.Game) return;
        if (material == null || settings.customVolumetricCloudShader == null) return;
        pass.Setup(renderer.cameraColorTarget);
        renderer.EnqueuePass(pass);
    }

    protected override void Dispose(bool disposing)
    {
        CoreUtils.Destroy(material);
        pass?.Dispose();
    }
}
public class CustomVolumetricCloudRenderPass : ScriptableRenderPass
{
    private CustomVolumetricCloud.CustomVolumetricCloudSettings settings;
    private Material material;

    private RenderTargetIdentifier _cameraColorTarget;

    private RenderTargetIdentifier _tempRT;
    private int _tempRTID = Shader.PropertyToID("_CustomVolumetricCloudTemp");

    private RenderTargetIdentifier _halfResRT;
    private int _halfResRTID  = Shader.PropertyToID("_HalfResCloudRT");


    public CustomVolumetricCloudRenderPass(CustomVolumetricCloud.CustomVolumetricCloudSettings settings, Material material)
    {
        this.settings = settings;
        this.material = material;
    }

    public void Setup(RenderTargetIdentifier cameraColorTarget)
    {
        this._cameraColorTarget = cameraColorTarget;
    }

    public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
    {
        var desc = renderingData.cameraData.cameraTargetDescriptor;
        desc.depthBufferBits = 0;
        desc.colorFormat = RenderTextureFormat.ARGBHalf; 

        // 全分辨率
        cmd.GetTemporaryRT(_tempRTID, desc, FilterMode.Bilinear);
        _tempRT = new RenderTargetIdentifier(_tempRTID);

        // 1/4分辨率fogRT(Raymarching在这里执行)
        desc.width /= 4;
        desc.height /= 4;
        cmd.GetTemporaryRT(_halfResRTID, desc, FilterMode.Bilinear);
        //cmd.GetTemporaryRT(_halfResRTID, desc, FilterMode.Point);
        _halfResRT = new RenderTargetIdentifier(_halfResRTID);


    }

    public void UpdateMaterialProperties(RenderingData renderingData)
    {
       // 获取 RuntimeData,没有则用 settings 作为回退
        var data = WeatherSystem.Instance?.RuntimeData;
        // if (data?.weatherMap != null)
        // {
        //     material.SetTexture("_WeatherMap", data.weatherMap);
        //     material.SetFloat("_WeatherMapScale", data.weatherMapScale);
        //     Debug.Log("云shader绑定的weatherMap实例ID: " + data.weatherMap.GetInstanceID());
        // }
      
        // ===== 相机矩阵 不走 RuntimeData=====
        Camera camera = renderingData.cameraData.camera;
        Matrix4x4 proj = GL.GetGPUProjectionMatrix(camera.projectionMatrix, false);
        Matrix4x4 viewMatrix = camera.worldToCameraMatrix;
        Matrix4x4 vp = proj * viewMatrix;
        Matrix4x4 inverseVP = vp.inverse;

        material.SetMatrix("_InverseVP", inverseVP);
        material.SetMatrix("_ProjectionMatrix", proj);
        material.SetMatrix("_CameraViewMatrix", viewMatrix);
        material.SetMatrix("_InverseProjection", proj.inverse);
        

        // ===== 固定资源(不走 RuntimeData,始终从 settings 读)=====
        material.SetTexture("_ShapeNoiseTex",  settings.cloudShapeNoiseTexture);
        material.SetTexture("_DetailNoiseTex", settings.cloudDetailNoiseTexture);
        material.SetTexture("_CirrusNoiseTex", settings.cirrusNoiseTexture);
        material.SetTexture("_CurlNoiseTex",   settings.curlNoiseTexture);
        material.SetTexture("_BlueNoiseTex",   settings.blueNoiseTexture);
        
        // ===== 天气参数(优先 RuntimeData,回退 settings)=====

        // 对流层云
        material.SetFloat("_CloudBottom",          data != null ? data.cloudBottom      : settings.cloudBottom);
        material.SetFloat("_CloudTop",             data != null ? data.cloudTop         : settings.cloudTop);
        material.SetFloat("_ShapeNoiseScale",      data != null ? data.shapeNoiseScale  : settings.shapeNoiseScale);
        material.SetFloat("_DetailNoiseScale",     data != null ? data.detailNoiseScale : settings.detailNoiseScale);
    
        material.SetFloat("_DetailErodeType",      data != null ? data.detailErodeType  : settings.detailErodeType);
        material.SetFloat("_DetailErodeIntensity", data != null ? data.detailErodeIntensity : settings.detailErodeIntensity);
        material.SetColor("_CloudColor",           data != null ? data.cloudColor       : settings.cloudColor);

        // 卷云
        material.SetFloat("_CirrusBottom",    data != null ? data.cirrusBottom     : settings.cirrusBottom);
        material.SetFloat("_CirrusScale",     data != null ? data.cirrusScale      : settings.cirrusScale);
        material.SetFloat("_CirrusIntensity", data != null ? data.cirrusIntensity  : settings.cirrusIntensity);
        material.SetFloat("_CirrusAbsorption",data != null ? data.cirrusAbsorption : settings.cirrusAbsorption);
        material.SetColor("_CirrusColor",     data != null ? data.cirrusColor      : settings.cirrusColor);

        // 地平线参数
        material.SetFloat("_AerialDensity", data != null ? data.aerialDensity : settings.aerialDensity);
        material.SetColor("_HorizonTint",   data != null ? data.horizonTint   : settings.horizonTint);
        
        // 风场
        material.SetFloat("_WindSpeed",            data != null ? data.cloudWindSpeed : settings.windSpeed);
        material.SetVector("_WindDirection",       data != null ? (Vector4)data.cloudWindDirection.normalized : (Vector4)settings.windDirection.normalized);
        material.SetFloat("_WeatherMoveSpeed",     data != null ? data.weatherMoveSpeed : settings.weatherMoveSpeed);
        material.SetVector("_WeatherMoveDirection",data != null ? (Vector4)(Vector3)data.weatherMoveDirection : (Vector4)settings.weatherMoveDirection);

        // 光照
        material.SetColor("_AmbientColor",          data != null ? data.ambientColor          : settings.ambientColor);
        material.SetFloat("_AmbientIntensity",      data != null ? data.ambientIntensity      : settings.ambientIntensity);
        material.SetFloat("_AmbientBaseIntensity",  data != null ? data.ambientBaseIntensity  : settings.ambientBaseIntensity);
        material.SetFloat("_AmbientFloor",          data != null ? data.ambientFloor          : settings.ambientFloor);
        material.SetFloat("_AmbientBottomStrength", data != null ? data.ambientBottomStrength : settings.ambientBottomStrength);
        material.SetFloat("_PowderEffectIntensity", data != null ? data.powderEffectIntensity : settings.powderEffectIntensity);
        
    
        // 渲染基础
        material.SetFloat("_Extinction",      data != null ? data.extinction       : settings.extinction);
        material.SetFloat("_Absorption",      data != null ? data.absorption       : settings.absorption);
        material.SetFloat("_ScatterIntensity",data != null ? data.scatterIntensity : settings.scatterIntensity);

        // HG 相位
        material.SetFloat("_Anisotropy",     data != null ? data.anisotropy      : settings.anisotropy);
        material.SetFloat("_SilverSpread",   data != null ? data.silverSpread    : settings.silverSpread);
        material.SetFloat("_SilverIntensity",data != null ? data.silverIntensity : settings.silverIntensity);

        // Multiple Scattering
        material.SetFloat("_MultipleScatteringDepthPower",  data != null ? data.msDepthPower : settings.multipleScatteringDepthPower);
        material.SetFloat("_MultipleScatteringHeightPower", data != null ? data.msHeightPower: settings.multipleScatteringHeightPower);
        material.SetFloat("_MultipleScatterIntensity",      data != null ? data.msIntensity  : settings.multipleScatterIntensity);

        // WeatherMap RT
        if (data?.weatherMap != null)
        {
            material.SetTexture("_WeatherMap",  data.weatherMap);
            material.SetFloat("_WeatherMapScale", data.weatherMapScale);
        }
        // ↓ 新增:Height Profile LUT
        var profileGen = HeightProfileGenerator.Instance;
        if (profileGen != null)
        {
            // 保底机制:如果 profileLUT 还没 Bake 出来,强行 Bake 一次
            if (profileGen.profileLUT == null)
            {
                profileGen.Bake();
            }

            if (profileGen.profileLUT != null)
            {
                material.SetTexture("_HeightProfileLUT", profileGen.profileLUT);
            }
        }

        // ===== 始终从 settings 读的参数(不受天气系统控制)=====
        material.SetFloat("_BottomType",      settings.bottomType);
        material.SetFloat("_TopType",         settings.topType);
        material.SetFloat("_DensityThreshold",settings.densityThreshold);
        material.SetFloat("_Coverage",        settings.coverage);
        material.SetFloat("_CloudType",       settings.cloudType);
        material.SetFloat("_CirrusType",      settings.cirrusType);
        material.SetFloat("_CirrusCoverage",  settings.cirrusCoverage);
        material.SetFloat("_CurlScale",       settings.curlScale);
        material.SetFloat("_CurlStrength",    settings.curlStrength);
        material.SetFloat("_BlueNoiseTileScale", settings.blueNoiseTileScale);
        material.SetFloat("_NoiseStrength",   settings.noiseStrength);
        material.SetFloat("_CloudShadowStrength", settings.cloudShadowStrength);
        material.SetFloat("_CloudShadowScale", settings.cloudShadowScale);
        

        Camera cam = renderingData.cameraData.camera;
        Light sun = RenderSettings.sun;
        Vector3 sunDir = sun != null ? -sun.transform.forward : Vector3.up;
        // 在太阳方向上取一个远点,投影到屏幕空间(Viewport 0~1坐标)
        Vector3 sunWorldPos = cam.transform.position + sunDir * 50000f;
        Vector3 sunViewportPos = cam.WorldToViewportPoint(sunWorldPos);
        
        material.SetVector("_SunScreenPos", new Vector4(sunViewportPos.x, sunViewportPos.y, sunViewportPos.z, 0));
        
        material.SetFloat("_GodRayDensity",     settings.godRayDensity);
        material.SetFloat("_GodRayDecay",       settings.godRayDecay);
        material.SetFloat("_GodRayWeight",      settings.godRayWeight);
        material.SetFloat("_GodRayIntensity",   settings.godRayIntensity);
        material.SetInt("_GodRaySampleCount",   settings.godRaySampleCount);
        material.SetFloat("_GodRayObjectContribution",   settings.godRayObjectContribution);
        

        if (settings.useStepJitter)
            material.SetInt("_UseStepJitter", 1);
        else
            material.SetInt("_UseStepJitter", 0);

    }
   
 
    
    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
    
        CommandBuffer cmd = CommandBufferPool.Get("CustomVolumetricCloudRenderPass");
        // 材质传参
        UpdateMaterialProperties(renderingData);
 
        if (settings.enablecloudShadow)
        {
            // cloudShadow
            Blit(cmd, _cameraColorTarget, _tempRT, material, 2); 
            Blit(cmd, _tempRT, _cameraColorTarget);
            // cloud
            Blit(cmd, _tempRT, _halfResRT, material, 0);
        }
        else
        {
            // cloud
            Blit(cmd, _cameraColorTarget, _tempRT);
            Blit(cmd, _cameraColorTarget, _halfResRT, material, 0);
        }
        
        // combine cloudShadow
        cmd.SetGlobalTexture("_HalfResCloudRT", _halfResRTID);
        Blit(cmd, _halfResRT, _tempRT, material, 1);          
       
        // combine GodRays
        if (settings.enableGodRays)
        {   
           
            Blit(cmd, _tempRT, _cameraColorTarget, material, 3); 
        }
        else
        {
            Blit(cmd, _halfResRT, _cameraColorTarget, material, 1);
        }
        
        // RT Blit
        // Blit(cmd, _cameraColorTarget, _halfResRT, material, 0);
        // cmd.SetGlobalTexture("_HalfResCloudRT", _halfResRTID);
        // Blit(cmd, _halfResRT, _cameraColorTarget, material, 1);


        context.ExecuteCommandBuffer(cmd);
        CommandBufferPool.Release(cmd);
    }
   
    // Cleanup any allocated resources that were created during the execution of this render pass.
    public override void OnCameraCleanup(CommandBuffer cmd)
    {
        cmd.ReleaseTemporaryRT(_tempRTID);
        cmd.ReleaseTemporaryRT(_halfResRTID);
    }
    public void Dispose()
    {
      
    }
}

VolumetricCloudShader 完整代码

Shader "Hidden/VolumetricCloud"
{
     Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" }
        Cull Off ZWrite Off ZTest Always

        HLSLINCLUDE
        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl"
        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareNormalsTexture.hlsl"
  

        float4 _MainTex_TexelSize;
        float4x4 _InverseVP;


        float _Anisotropy;
        float _SilverSpread;
        float _SilverIntensity;
        float _MultipleScatteringDepthPower;
        float _MultipleScatteringHeightPower;
     
        float _ScatterIntensity;
        
        float _NoiseStrength;
       
        int _UseStepJitter;
        float _BlueNoiseTileScale;

       
        //========== 积云层 ========
        float _ShapeNoiseScale;
        float _DetailNoiseScale;
        float _CloudBottom;
        float _CloudTop;
        float _BottomType;
        float _TopType;

        float _DensityThreshold;
        float _Extinction;
        float _Absorption;
        float _Coverage;
       
        float _DetailErodeIntensity;
        float _DetailErodeType;
        float4 _CloudColor;
        //======== Wind =========
        float3 _WindDirection;
        float _WindSpeed;
        float3 _WeatherMoveDirection;
        float _WeatherMoveSpeed;

        //========= 卷云层 =========
        float _CirrusType;
        float _CirrusCoverage;  //未使用
        float _CirrusAbsorption;
        float _CloudType;       //未使用
        float _MultipleScatterIntensity;
        float4 _CirrusColor;
        //========= 环境光 =========
        float _AmbientBaseIntensity;
        float _AmbientFloor;
        float _AmbientBottomStrength;
        float4 _AmbientColor;
        float _AmbientIntensity;
        float _PowderEffectIntensity;
        //========= 地平线参数 =========
        float _AerialDensity;
        float4 _HorizonTint;
        
        //========= 扰动 ===========
        float _CirrusBottom;
        float _CirrusScale;
        float _CirrusIntensity;
        float _CurlScale;
        float _CurlStrength;

        //========= ShadowMap ===========
        TEXTURE2D(_CloudShadowMap);
        SAMPLER(sampler_CloudShadowMap);
        float _CloudShadowStrength;
        TEXTURE2D(_CirrusNoiseTex);
        SAMPLER(sampler_CirrusNoiseTex);
        TEXTURE2D(_CurlNoiseTex);
        SAMPLER(sampler_CurlNoiseTex);

        TEXTURE2D(_MainTex); 
        SAMPLER(sampler_MainTex);
       
        TEXTURE3D(_ShapeNoiseTex);
        SAMPLER(sampler_ShapeNoiseTex);

        TEXTURE3D(_DetailNoiseTex);
        SAMPLER(sampler_DetailNoiseTex);

        TEXTURE2D(_HalfResCloudRT);
        SAMPLER(sampler_HalfResCloudRT);

        TEXTURE2D(_SceneColor);
        SAMPLER(sampler_SceneColor);
        
        TEXTURE2D(_BlueNoiseTex);
        SAMPLER(sampler_BlueNoiseTex);
        float4 _BlueNoiseTex_TexelSize;
        
        TEXTURE2D(_HeightProfileLUT);
        SAMPLER(sampler_HeightProfileLUT);
        
        TEXTURE2D(_WeatherMap);
        SAMPLER(sampler_WeatherMap);
        float _WeatherMapScale;

        #if defined(_VolumetricFog_QUALITY_MOST)
            static const int SAMPLE_COUNT = 128;
        #elif defined(_VolumetricFog_QUALITY_HIGH)
            static const int SAMPLE_COUNT = 64;
        #elif defined(_VolumetricFog_QUALITY_MEDIUM)
            static const int SAMPLE_COUNT = 32;
        #else
            static const int SAMPLE_COUNT = 16;
        #endif
        struct Attributes
        {
            float4 positionOS : POSITION;
            float2 uv : TEXCOORD0;
            //UNITY_VERTEX_INPUT_INSTANCE_ID  //VR兼容 
        };

        struct Varyings
        {
            float4 positionCS : SV_POSITION;
            float2 uv : TEXCOORD0;
            //UNITY_VERTEX_OUTPUT_STEREO      //VR兼容 
        };

        Varyings vert(Attributes input)
        {
            Varyings output;
            //UNITY_SETUP_INSTANCE_ID(input);     //VR兼容         
            //UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output); 
            output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
            output.uv = input.uv;
            return output;
        }

        float remap(float v, float oldMin, float oldMax, float newMin, float newMax)
        {
            return newMin + (v - oldMin) / (oldMax - oldMin) * (newMax - newMin);
        }
        ENDHLSL


        // Pass 0: CustomVolumetricCloud
        Pass
        {
            Name "CustomVolumetricCloud"
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
           
           
            // 相位函数
            float HenyeyGreenstein(float cosTheta, float g)
            {
                float g2 = g * g;
                return (1.0 - g2) / (4.0 * PI * pow(1.0 + g2 - 2.0 * g * cosTheta, 1.5));
            }
            // 升级版:双向海尼-格林斯坦相位函数
            float DualHenyeyGreenstein(float cosTheta, float g, float silverIntensity, float silverSpread)
            {
                float primary   = HenyeyGreenstein(cosTheta, g);
                float secondary = HenyeyGreenstein(cosTheta, 0.99 - silverSpread) * silverIntensity; // 银边:接近1的g值
                return max(primary,  secondary);
            }
           
            float sampleLightTransmittance(float3 samplePos, float dimensional_profile)
            {
                // URP 获取主光源方向
                Light mainLight = GetMainLight();
                float3 lightDir = mainLight.direction;
                
                float totalDensity = 0;
                float lightStepSize = 50;  // 光照采样基础步长
                
                // 6个采样点,步长逐渐翻倍
                float stepScales[6] = {1, 2, 4, 8, 16, 32};
                
                float3 windOffset = _WindDirection * _Time.y * _WindSpeed;
                
                [unroll]
                for (int i = 0; i < 6; i++)
                {
                    float3 currentSamplePos = samplePos + lightDir * lightStepSize * stepScales[i];
                    float3 animatedPos = currentSamplePos + windOffset;
                    // 只用 cheap 采样(不加 Detail Noise)
                    // 和主步进保持一致的最基础处理
                  
                    float4 shape = SAMPLE_TEXTURE3D(_ShapeNoiseTex, sampler_ShapeNoiseTex, animatedPos * _ShapeNoiseScale);
                    float perlinWorley = shape.r;
                    float worleyFBM = shape.g * 0.625 + shape.b * 0.25 + shape.a * 0.125;
                    float baseCloud = saturate(remap(perlinWorley, worleyFBM - 1.0, 1.0, 0.0, 1.0));

                    float base_cloud_density = saturate(baseCloud - (1.0 - dimensional_profile));
                    totalDensity += base_cloud_density;
                }
                
                // Beer-Lambert:密度越大透射率越低
                return exp(-totalDensity * _Absorption);
            }
            float SampleCloudProfile(float heightFraction, float cloudType)
            {
                float2 uv = float2(cloudType, heightFraction);
                return saturate(SAMPLE_TEXTURE2D_LOD(_HeightProfileLUT, sampler_HeightProfileLUT, uv, 0).r);
            }
          
            float sampleLightTransmittance2(float3 samplePos, float3 earthCenter, float earthRadius, float density_along_view)
            {
                Light mainLight = GetMainLight();
                float3 lightDir = mainLight.direction;
                //float layerThickness = _CloudTop - _CloudBottom;      // 6500
                //float MaxLightStepSize  = layerThickness / 63.0;  
                //float MinLightStepSize = 50.0;
                float totalDensity = 0;

                const float nearStep = 40.0;
                static const float posScales[6]  = {1, 2, 3, 4, 24, 56};
                static const float segLengths[6] = {40, 40, 40, 40, 800, 1280};
                const float invTotalLength = 1.0 / 2240.0;
                //float stepScales[10] = {1, 2, 4, 8, 16, 32, 64, 128, 256, 512};
                float3 windOffset = _WindDirection * _Time.y * _WindSpeed;
   
                [loop]
                for (int i = 0; i < 6; i++)
                {
                    float3 p = samplePos + lightDir * nearStep * posScales[i];
                   
                    // 每个采样点重算高度轮廓
                    float distToCenter = length(p - earthCenter);
                    float heightFraction = saturate((distToCenter - (earthRadius + _CloudBottom)) / (_CloudTop - _CloudBottom));

                    // 每点重采 weather
                    float2 wUV = p.xz / _WeatherMapScale + 0.5 + _WeatherMoveDirection.xz * _WeatherMoveSpeed * _Time.y;
                    float4 w = SAMPLE_TEXTURE2D_LOD(_WeatherMap, sampler_WeatherMap, wUV, 2); 
                    // 采样HeightProfile
                    float cloudHeightProfile = SampleCloudProfile(heightFraction, w.g);  
                    float profile = cloudHeightProfile * _Coverage;
                    
                    int mip = (density_along_view < 0.3) ? i / 2 : 2;
                    float3 animatedPos = p + windOffset;
                    float4 shape = SAMPLE_TEXTURE3D_LOD(_ShapeNoiseTex, sampler_ShapeNoiseTex, animatedPos * _ShapeNoiseScale, mip);
                    float worleyFBM = shape.g * 0.625 + shape.b * 0.25 + shape.a * 0.125;
                    float baseCloud = saturate(remap(shape.r, worleyFBM - 1.0, 1.0, 0.0, 1.0));
                    float baseCloudDensity = saturate(baseCloud - (1.0 - profile));

                    //totalDensity += saturate(baseCloud - (1.0 - profile)) * (stepScales[i]);  // 乘段长权重
                    totalDensity += baseCloudDensity * segLengths[i] * invTotalLength * 6.0;
                    if (totalDensity * _Absorption > 24.0) break; 
                }
                return exp(-totalDensity * _Absorption);
            }
           
            float sampleCirrusLightTransmittance(float3 cirrusPos, float2 windUVOffset, float2 curlOffset)
            {
                Light mainLight = GetMainLight();
                float3 lightDir = mainLight.direction;
                
                float totalDensity = 0;
                float lightStepSize = 200.0; // 卷云层很薄,步长可以大一点
                
               //float2 windUVOffset = _WindDirection.xz * _Time.y * _WindSpeed * 0.001;
                
                // 4个采样点,步长逐渐翻倍
                float stepScales[4] = {1, 2, 4, 8};
                
                [unroll]
                for (int i = 0; i < 4; i++)
                {
                    float3 samplePos = cirrusPos + lightDir * lightStepSize * stepScales[i];
                    float2 sampleUV =  (samplePos.xz + curlOffset) * _CirrusScale + windUVOffset;
                    
                    float3 cirrusSample = SAMPLE_TEXTURE2D(_CirrusNoiseTex, sampler_CirrusNoiseTex, sampleUV).rgb;

                    float2 weatherUV = saturate(cirrusPos.xz / _WeatherMapScale + 0.5);
                    float4 weather = SAMPLE_TEXTURE2D(_WeatherMap, sampler_WeatherMap, weatherUV);

                    float cirrusCoverage = weather.b * _CirrusCoverage;
                    float cirrusType = weather.a;
                    
                    float cd = remap(cirrusType, 0.5, 1.0, remap(cirrusType, 0.0, 0.5, cirrusSample.r, cirrusSample.g), cirrusSample.b);


                    cd = pow(max(cd, 0.0), 1.0 - remap(cirrusCoverage, 0.0, 1.0, -0.9, 0.9));
                    cd *= remap(pow(cirrusCoverage, 3.0), 0.0, 0.5, 0.0, 1.0);
                    cd = saturate(cd);
                    
                    totalDensity += cd;
                }
                return exp(-totalDensity * _CirrusAbsorption);
                //return exp(-totalDensity * _CirrusAbsorption);
            }
            // 球形相交函数
            bool raySphereIntersect(float3 rayOrigin, float3 rayDir, float3 center, float radius, out float t0, out float t1)
            {
                float3 oc = rayOrigin - center;
                float b = dot(rayDir, oc);
                float c = dot(oc, oc) - radius * radius;
                float disc = b * b - c;
                if (disc < 0) { t0 = -1; t1 = -1; return false; }
                float sqrtDisc = sqrt(disc);
                t0 = -b - sqrtDisc;
                t1 = -b + sqrtDisc;
                return true;
            }
            
            float4 frag(Varyings input) : SV_Target
            {
                float2 uv = input.uv;
                
                float rawDepth = SampleSceneDepth(uv);
                float depth = LinearEyeDepth(rawDepth, _ZBufferParams);
                //利用从 C# 传过来的 _InverseVP 重建世界空间坐标
                float4 ndc = float4(uv * 2.0 - 1.0, rawDepth, 1.0);
                float4 worldPos = mul(_InverseVP, ndc);
                worldPos /= worldPos.w;

                // if (rawDepth == 0.0)
                // {
                //    return float4(0, 0, 0, 1.0);
                // }    

                bool isSky = false;
                #if defined(UNITY_REVERSED_Z)
                    if (rawDepth <= 0.00001) isSky = true;
                #else
                    if (rawDepth >= 0.99999) isSky = true;
                #endif

                if (!isSky && depth < _CloudBottom * 0.5)   // 近处不透明物挡住,云不可能在其前
                return float4(0, 0, 0, 0);
                // ======== 射线步进 ========
                // 射线终点:worldPos.xyz(当前像素对应的世界坐标)
                // 射线起点:相机世界坐标_WorldSpaceCameraPos(URP内置变量,不需要额外传)
                float3 rayStart = _WorldSpaceCameraPos;
                float3 rayEnd = worldPos.xyz;
                float3 rayDir = normalize(rayEnd - rayStart);

                // //剔除地平线向下的射线
                // if (rayDir.y < -0.001)
                //     return float4(0, 0, 0, 0);

                // 地球中心在相机正下方
                float earthRadius = 6371000.0;
                float3 earthCenter = float3(_WorldSpaceCameraPos.x,  -earthRadius, _WorldSpaceCameraPos.z);

                float t0Inner, t1Inner, t0Outer, t1Outer;
                bool hitInner = raySphereIntersect(rayStart, rayDir, earthCenter, earthRadius + _CloudBottom, t0Inner, t1Inner);
                bool hitOuter = raySphereIntersect(rayStart, rayDir, earthCenter, earthRadius + _CloudTop, t0Outer, t1Outer);
                
               
                // 没打到外球,说明射线向下,不渲染
                if (!hitOuter) return float4(0, 0, 0, 0);
                if (_WorldSpaceCameraPos.y < _CloudBottom && rayDir.y < -0.001)
                return float4(0, 0, 0, 0);

                float tMin, tMax;
                
                if (_WorldSpaceCameraPos.y < _CloudBottom)
                {
                    // 相机在云层下方(最常见)
                    tMin = t1Inner; // 从内球出口进入云层
                    tMax = t1Outer; // 到外球出口离开云层
                }
                else if (_WorldSpaceCameraPos.y > _CloudTop)
                {
                    // 相机在云层上方
                    tMin = t0Outer; // 从外球入口进入云层
                    tMax = t0Inner > 0 ? t0Inner : t1Outer; // 到内球入口或外球出口
                }
                else
                {
                    // 相机在云层内部:起点就是相机自身
                    tMin = 0;                
                    if (rayDir.y >= 0)
                    {
                        // 向上看,终点是外球的出口
                        tMax = t1Outer;
                    }
                    else
                    {
                        // 向下看
                        if (hitInner && t0Inner > 0)
                        {
                            // 如果撞击到了内球(云底下方空间),步进到内球入口为止
                            tMax = t0Inner;
                        }
                        else
                        {
                            // 视线倾斜擦过地平线但没撞到内球的情况,用外球出口兜底
                            tMax = t1Outer;
                        }
                    }
                }      
                
                //保持场景在前,云在后
                if (!isSky)
                {
                    float maxRayDist = length(rayEnd - rayStart);
                    tMax = min(tMax, maxRayDist);
                }
                
                //保持场景在前,云在后
                if (tMax <= tMin) return float4(0, 0, 0, 0);

                if (tMax < 0 || tMin > tMax)
                    return float4(0, 0, 0, 0);
                tMin = max(tMin, 0);

               
                // 限制云的最大可见距离,避免水平视角下步进过于稀疏
                float maxCloudDist = 15000.0;  // 根据你的场景尺度调整
                tMax = min(tMax, tMin + maxCloudDist);

                
                // ============ 射线步进循环参数 =======
                float transmittance = 1.0;
                float3 scatteredLight = float3(0, 0, 0);

                // ============== 平行光 ============
                Light mainLight = GetMainLight();
                float3 lightDir = mainLight.direction;

                // ============== 环境光 ============
                // float3 ambient = unity_AmbientSky.rgb * _AmbientIntensity; 内置skyColor
                float3 ambient = _AmbientColor.xyz * _AmbientIntensity;
                //float3 ambientScatter = ambient * (1.0 / (4.0 * PI));
               
                // ========== 积云-双相位函数 ==============
                float cosTheta = dot(rayDir, mainLight.direction);
                float phase = DualHenyeyGreenstein(cosTheta, _Anisotropy, _SilverIntensity, _SilverSpread);

                // ========== WindOffset ==============
                float3 windOffset       = _WindDirection * _Time.y * _WindSpeed;
                float3 detailWindOffset = _WindDirection * _Time.y * _WindSpeed * 0.4;
                float2 weatherOffset    = _WeatherMoveDirection.xz * _WeatherMoveSpeed * _Time.y;

                // ============ 步进步长参数 ===========
                // int stepCount = 32;
                // float stepSize = (tMax - tMin) / stepCount;
                //float horizonFactor = 1.0 - abs(rayDir.y);
                //int stepCount = (int)lerp(128, 512, horizonFactor);  // 水平视角时步数增加到96
                // 固定每步的世界空间距离
                // float fixedStepSize = 25.0; // 单位:米,根据你的场景尺度调整
                // int stepCount = (int)clamp((tMax - tMin) / fixedStepSize, 32, 512);
                // float stepSize = (tMax - tMin) / stepCount;

                // ========= 步进步长参数 - 穿云判断 =======
                // 判断相机是否在云层内
                bool cameraInCloud = (_WorldSpaceCameraPos.y > _CloudBottom && _WorldSpaceCameraPos.y < _CloudTop);

                // 相机在云内用更小的初始步长
                //float near_step_size = cameraInCloud ? 1.0 : 3.0;
                float near_step_size = 3.0;
                float far_step_size_offset = 60.0;
                float step_adjustment_distance = 16384.0;
                float stepScale = 1.0; 
                float lastStride = 0.0;  
                // ========= 射线步进起点抖动偏移 =======
                float tMinJitter = 0.0;
                if(_UseStepJitter)
                {
                    //float jitter1 = frac(sin(dot(uv, float2(12.9898, 78.233))) * 43758.5453);
                    float2 noiseUV = input.uv * _ScreenParams.xy * _BlueNoiseTex_TexelSize.xy;
                    float jitter = SAMPLE_TEXTURE2D(_BlueNoiseTex, sampler_BlueNoiseTex, noiseUV * _BlueNoiseTileScale).r;
                    //tMinJitter = jitter * near_step_size; 
                    float stepSizeAtTMin = near_step_size + (far_step_size_offset * tMin / step_adjustment_distance);
                    tMinJitter = jitter * stepSizeAtTMin;    // 远处入云时抖动自动放大到 ~60m
                    tMin += tMinJitter;
                }

                // 偏移到初始采样位置系数
                float t = tMin;

                float prevStride = near_step_size;
                [loop]
                for (int i = 0; i < 512; i++)
                {
                  
                    if (t >= tMax) break; // 防御:防止抖动导致最后一步越界超过不透明物体
                    //if (transmittance < 0.01) break;

                    // 当前步长(近处3m,16km处63m)
                    //float stepSize = near_step_size + (far_step_size_offset * t / step_adjustment_distance);

                    float rawStepSize = near_step_size + (far_step_size_offset * t / step_adjustment_distance);
                    float stepSize = lerp(prevStride, rawStepSize, 0.5);
                    float3 samplePos = rayStart + rayDir * t;
                  
                    // ===== 高度梯度(常规版) ===
                    //float heightFraction = saturate((samplePos.y - _CloudBottom) / (_CloudTop - _CloudBottom));
                    // ===== 高度梯度(球壳版)=====
                    float distToCenter = length(samplePos - earthCenter);
                    float heightFraction = saturate((distToCenter - (earthRadius + _CloudBottom)) /  (_CloudTop - _CloudBottom));
                 
                    // 从 Weather Map 采样 cloudType(暂时还没有的话先用全局参数)
                    //float topType = _CloudType; // 之后替换为 weatherMap.b
                    // ================================================
                    // ============== Weather Map 采样 =================
                    // ================================================
                   // float weatherMip = clamp(log2(max(t, 1.0) / maxCloudDist), 0.0, 4.0);
                    float2 weatherUV = samplePos.xz / _WeatherMapScale + 0.5 + weatherOffset;
                    float4 weather = SAMPLE_TEXTURE2D_LOD(_WeatherMap, sampler_WeatherMap, weatherUV, 2);
                    //weather = float4(0.86, 0.9, 0.9, 1);
                    float coverage  = weather.r * _Coverage;
                    float cloudType = weather.g;
                    //return float4(coverage.xxx, 1); // 归一化到可见范围,方便对比亮度
                
                    // TopType 和 BottomType 从 cloudType 派生
                    float topType    = saturate(cloudType);
                    float bottomType = saturate(cloudType * 0.8);

                    // ===============================================================                 
                    // ============ 定义的三类云的垂直基础轮廓 heightGradient ==========
                    // ===============================================================
                  
                   
                    // // 层云 Stratus:从 0.05 开始淡入,0.1 达到最浓;0.15 开始淡出,0.22 彻底消失 (很薄)
                    // float stratusBottomFade = remap(heightFraction, 0, 0.03, 0.0, 1.0);
                    // float stratusTopFade = remap(heightFraction, 0.15, 0.25, 1.0, 0.0);

                    // // 层积云 Stratocumulus:从 0.05 开始淡入,0.15 达到最浓;从 0.35 开始淡出,0.5 彻底消失 (中等)
                    // float stratocumulusBottomFade = remap(heightFraction, 0.03, 0.15, 0.0, 1.0);
                    // float stratocumulusTopFade = remap(heightFraction, 0.35, 0.65, 1.0, 0.0);

                    // // 积云 Cumulus:从 0.05 开始淡入,0.2 达到最浓;从 0.6 开始淡出,0.85 彻底消失 (高耸)
                    // float cumulusBottomFade = remap(heightFraction, 0.1, 0.25, 0.0, 1.0);
                    // float cumulusTopFade = remap(heightFraction, 0.6, 0.95, 1.0, 0.0);

                    // // blend topGradient
                    //float cloudHeightGradient = 0.0;
                    // float t1 = saturate(topType * 2.0);
                    // float t2 = saturate((topType - 0.5) * 2.0);
                    // // float topGradient = lerp(stratusTopFade, max(stratusTopFade, stratocumulusTopFade), t1);
                    // // topGradient = lerp(topGradient, max(topGradient, cumulusTopFade), t2);
                    // float topGradient = lerp(
                    //     lerp(stratusTopFade, stratocumulusTopFade, t1),
                    //     cumulusTopFade,
                    //     t2
                    // );
                    // // blend bottomGradient
                    // float b1 = saturate(bottomType * 2.0);
                    // float b2 = saturate((bottomType - 0.5) * 2.0);
                    // // float  bottomGradient = lerp(stratusBottomFade, max(stratusBottomFade, stratocumulusBottomFade), b1);
                    // // bottomGradient = lerp(bottomGradient, max(bottomGradient, cumulusBottomFade), b2);
                    // float bottomGradient = lerp(
                    //     lerp(stratusBottomFade, stratocumulusBottomFade, b1),
                    //     cumulusBottomFade,
                    //     b2
                    // );
                    // ========= final heightGradient 基本云轮廓 =========
                    //cloudHeightGradient = saturate(topGradient) * saturate(bottomGradient);
                    //cloudHeightGradient = saturate(heightGradient);
                    
                    // 采样HeightProfile
                    float cloudHeightProfile = SampleCloudProfile(heightFraction, cloudType);   // 第0行 = stratusBottom
                    // ======== final 云轮廓 =========
                    float dimensional_profile = saturate(cloudHeightProfile * coverage);
                   
                

                    // ===== 穿云处理1:球形消隐 =====
                    float camSphereMask = 1.0;
                    if (cameraInCloud)
                    {
                        // 采样点到相机的世界空间距离
                        float distToCam = length(samplePos - _WorldSpaceCameraPos);
                        camSphereMask = saturate(distToCam / 80.0);
                    }

                    dimensional_profile *= camSphereMask;

                    // // 一级continue
                    // if (dimensional_profile <= 0.001)
                    // {
                    //     t += stepSize;
                    //     continue; 
                    // }

                    // 一级 continue -改
                    if (dimensional_profile <= 0.05)
                    {
                        lastStride = stepSize * stepScale;
                        t += lastStride;
                        stepScale = min(stepScale * 1.5, 4.0);
                        continue;   // 连续踩空,步子越迈越大,封顶4倍
                    }
                    // 命中云时:
                    if (stepScale > 1.0)
                    {
                        t -= lastStride;  // 回退,找回被大步跳过的云边
                        stepScale = 1.0;
                        t += stepSize * stepScale;    
                        continue;   
                    }
                   

                    // ===============================================================
                    // ============= Cheap 采样(Shape Noise 只读 r 通道)=============
                    // ===============================================================
                    // 只读取 R 通道 (.r),并且强制走低分辨率的 Mip 1 (最   后一个参数传 1.0)
                    float3 animatedPos = samplePos + windOffset;
                    float cheapShapeR = SAMPLE_TEXTURE3D_LOD(_ShapeNoiseTex, sampler_ShapeNoiseTex, animatedPos * _ShapeNoiseScale, 1.0).r;
                    float cheapCloud = remap(cheapShapeR, -0.5, 1.0, 0.0, 1.0);
                    //  不需计算任何 Worley FBM 直接拿R 通道去碰天气图的边界
                    //float cheapDensity = saturate(dimensional_profile - cheapShapeR);
                    float cheapDensity = saturate(cheapCloud - (1.0 - dimensional_profile));
                    if (cheapDensity <= 0.0)
                    {
                        t += stepSize;
                        continue;
                    }
                    // ===============================================================
                    // ================== 一级 Full 采样(baseCloud)==================
                    // ===============================================================
                    float shapeMip  = clamp(log2(max(t, 1.0) / maxCloudDist), 0.0, 4.0);
                    float detailMip = min(shapeMip, 1.0);   // detail 只有 32³,高 mip 全是灰,封顶 1
      
                    float4 shape = SAMPLE_TEXTURE3D_LOD(_ShapeNoiseTex, sampler_ShapeNoiseTex, animatedPos * _ShapeNoiseScale, 2);
                    float worleyFBM = shape.g * 0.625 + shape.b * 0.25 + shape.a * 0.125;
                    float baseCloud = remap(shape.r, worleyFBM - 1.0, 1.0, 0.0, 1.0);
                    baseCloud = saturate(baseCloud);

                    // 乘上高度梯度
                    float base_cloud_density = saturate(baseCloud - (1.0 - dimensional_profile));
                   
                    //base_cloud_density = saturate(pow(base_cloud_density,2) * 5);
                    // 二级continue 空区域:直接跳过
                    if (base_cloud_density <= 0.01)
                    {
                        t += stepSize;
                        continue;
                    }
        
                    // ===============================================================
                    // ================= 二级Full 采样(Detail + Curl)================
                    // ===============================================================
                             
                    float detailFBM;
                    float3 detailAnimatedPos = samplePos + detailWindOffset;
                    // curl Noise
                    float2 curlUV = samplePos.xz * _CurlScale;
                    float2 curl = SAMPLE_TEXTURE2D(_CurlNoiseTex, sampler_CurlNoiseTex, curlUV).rg * 2.0 - 1.0;
                    detailAnimatedPos += float3(curl.x, 0, curl.y) * _CurlStrength;
                    if (t < maxCloudDist - 1000.0)   
                    {                                         
                        float3 detail = SAMPLE_TEXTURE3D_LOD(_DetailNoiseTex, sampler_DetailNoiseTex, detailAnimatedPos * _DetailNoiseScale, 0).rgb;
                        detailFBM = detail.r * 0.625 + detail.g * 0.25 + detail.b * 0.125;
                               
                        // 淡出过渡:临近边界时向 0.5 收敛,避免分界线
                        //float detailFade = saturate((maxCloudDist - 500 - t) / 1000.0);   // 最后 1km 渐变
                
                    }
                    else
                    {   // 降LOD
                        float3 detail = SAMPLE_TEXTURE3D_LOD(_DetailNoiseTex, sampler_DetailNoiseTex, detailAnimatedPos * _DetailNoiseScale, 2).rgb;
                        detailFBM = detail.r * 0.625 + detail.g * 0.25 + detail.b * 0.125;              
                    }
                   
                    // height Detail Blend Tpye
                    float tpye1 = lerp(detailFBM, (1.0 - detailFBM), saturate( heightFraction * heightFraction));
                    float tpye2 = lerp(detailFBM, (1.0 - detailFBM), saturate( heightFraction * 8));
                    float tpye3 = max(tpye1, tpye2);
                    float tpye4 = min(tpye1, tpye2);

                    float t1 = saturate(_DetailErodeType * 3.0);
                    float t2 = saturate((_DetailErodeType - 0.33) * 3.0);
                    float t3 = saturate((_DetailErodeType - 0.66) * 3.0);
                    float heightDetailBlend = lerp(lerp(lerp(tpye1, tpye2, t1), tpye3, t2), tpye4, t3);

                    float oneMinusShape = 1.0 - base_cloud_density;                          
                    float detailErodeWeight = oneMinusShape * oneMinusShape * oneMinusShape;
                    base_cloud_density = base_cloud_density - (1.0 - heightDetailBlend) * detailErodeWeight  * _DetailErodeIntensity;
                   
                    float density = saturate(base_cloud_density);  
                    density = max(0, density - _DensityThreshold);

                    // ===== 穿云处理2:智能近剪裁面消隐 =====
                    // 如果相机在云外,保持短距离消隐避免硬切面;在云内时,放开限制(只保留极其微小的裁剪面遮罩)
                    float fadeDist = cameraInCloud ? 0.5 : 15.0;
                    float nearFade = saturate(t / fadeDist);
                    density *= nearFade;

                    // 三级continue 空区域:直接跳过
                    if (density <= 0)
                    {
                        t += stepSize;
                        continue;
                    }

                    float densityAlongView = 1.0 - transmittance;
                    // 光照透射率(这个点能接收多少光)
                    //float lightT = sampleLightTransmittance1(samplePos, dimensional_profile,densityAlongView);
                    //float lightT = sampleLightTransmittance(samplePos, dimensional_profile);
                    
                    float lightT = sampleLightTransmittance2(samplePos, earthCenter, earthRadius, densityAlongView);
                    float powderEffect = 1.0 - exp(-density * stepSize * 2.0) * _PowderEffectIntensity;
                 
                    // powderEffect = lerp(1.0, powderEffect, saturate(dimensional_profile)) *0.1;
                    //lightT *= powderEffect;
                    // // In-Scatter Probability (搁置废弃)
                    // float depth_probability = 0.05 + pow(density, remap(heightFraction, 0.3, 0.85, 0.5, 2.0));
                    // float vertical_probability = pow(remap(heightFraction, 0.07, 0.14, 0.1, 1.0), 0.8);
                    // float inScatterProb = depth_probability * vertical_probability;
                    // float inScatterProb = 1.0 - exp(-density * stepSize * _Extinction);       
                                    
                    // 直接散射 Direct Scattering
                    float primary_attenuation   = lightT;                       // 透射率
                    float secondary_attenuation = pow(lightT, 0.25) * 0.7;      // 衰减更慢的版本
                    float attenuation = max(primary_attenuation, remap(cosTheta, 0.7, 1.0, primary_attenuation, secondary_attenuation));
                    attenuation = attenuation * lerp(1.0, powderEffect, saturate(1.0 - attenuation));
                    float directScatter = phase * attenuation;
                    float3 directLight  = directScatter * mainLight.color.rgb ;
            
                    // Multiple Scattering
                    float ms_volume = remap(dimensional_profile * stepSize, 0.1, 1.0, 0.0, 1.0) * pow(coverage * cloudType, 0.25) ;
             
                 
                    //float ms_volume = remap(dimensional_profile * stepSize, 0.1, 1.0, 0.0, 1.0);
                    //float ms_volume = remap(heightFraction, 0.1, 1.0, 0.0, 1.0);
                    ms_volume *= pow(lightT, _MultipleScatteringDepthPower);
                    ms_volume *= pow(heightFraction, _MultipleScatteringHeightPower);
                    float secondaryPhase = HenyeyGreenstein(cosTheta, 0.3);
                    float3 multipleScatterLight = ms_volume * secondaryPhase * mainLight.color.rgb * _MultipleScatterIntensity ;
                    
                       
                    // ambient Scatter 环境散射
                    //float ambientScatter = pow(1.0 - dimensional_profile, 0.5);
                    //float ambientScatter = lerp(_AmbientFloor, 1.0, pow(1.0 - dimensional_profile, 0.5)) * lerp(_AmbientBottomStrength, 1.0, heightFraction);
                    float ambientScatter = lerp(_AmbientBaseIntensity, 1.0, smoothstep(_AmbientFloor, 1.0, 1.0 - dimensional_profile)) * lerp(_AmbientBottomStrength, 1.0, heightFraction);                  
                    float3 ambientLight = ambientScatter * ambient;
                    //float powderEffect = 1.0 - exp(-density * stepSize * 2.0) * _PowderEffectIntensity;
                    //ambientLight = ambientLight + ambientLight * powderEffect;
                    // aerial Color 极远处颜色渐变
                    float aerial = exp(-t * _AerialDensity);  // 0.00005~0.0002 
                    float3 aerialColor = lerp(_HorizonTint.rgb, _CloudColor.rgb, aerial);
                                    
                    // 累积散射光
                    float extinctionCoeff = density * _Extinction;
                    float stepTransmittance = exp(-extinctionCoeff * stepSize);
                    float integrationFactor = (extinctionCoeff > 0.0001) ? (1.0 - stepTransmittance) / extinctionCoeff  : stepSize;

                    scatteredLight += transmittance * (directLight + multipleScatterLight + ambientLight)  * density  * _ScatterIntensity  * integrationFactor * aerialColor  ;
                 
            
               
                    transmittance *= stepTransmittance;                                                        
                    t += stepSize;
                    prevStride = stepSize;
                    
                    if (transmittance < 0.01) break;  // early exit 
                }
                scatteredLight *= _CloudColor.rgb;
                // ===============================================================
                // =========================== 卷云层 =============================
                // ===============================================================
                //if (rayDir.y > 0.0001)  // 只处理向上看的射线
                //最后一步进行单次采样卷云
                if (transmittance > 0.01) 
                {
                    float cirrusHeight = _CirrusBottom;
                    float cirrusT = 0;
                    if(cirrusHeight - rayStart.y > 0)
                    {
                        cirrusT = -(cirrusHeight - rayStart.y) / (rayDir.y + 0.00001);
                    }
                    cirrusT = (cirrusHeight - rayStart.y) / (rayDir.y + 0.00001);
                    //float cirrusHeight = min(_CirrusBottom, _CloudTop - 200);
                    bool canRenderCirrus = (abs(rayDir.y) > 1e-4) && (cirrusT > 0) && (transmittance > 0.01);
                   
                    // 如果未射向天空(撞到了非天空物体,如地面),需确保卷云在物体前面
                    if (!isSky)
                    {
                        float maxRayDist = min(length(rayEnd - rayStart),maxCloudDist);
                        if (cirrusT > maxRayDist) canRenderCirrus = false;
                    }
                    if (canRenderCirrus)
                    {
                    
                        float3 cirrusPos = rayStart + rayDir * cirrusT;

                        float2 weatherUV = saturate(cirrusPos.xz / _WeatherMapScale + 0.5);
                        float4 weather = SAMPLE_TEXTURE2D(_WeatherMap, sampler_WeatherMap, weatherUV);

                        float cirrusCoverage = weather.b * _CirrusCoverage;
                        float cirrusType = weather.a;
                        
                       
                        if (cirrusCoverage > 0.05)
                        {
                            // Curl Noise 扰动
                            float2 curlUV = cirrusPos.xz * _CurlScale;
                            float2 curl = SAMPLE_TEXTURE2D(_CurlNoiseTex, sampler_CurlNoiseTex, curlUV).rg * 2.0 - 1.0;

                            float2 curlOffset = curl * _CurlStrength;
                            float2 windUVOffset = _WindDirection.xz * _Time.y * _WindSpeed * 0.001;

                            float2 cirrusUV = (cirrusPos.xz  + curlOffset) * _CirrusScale + windUVOffset;
    
                            // 采样三通道(streaky/wispy/round 分别存在 rgb)
                            float3 cirrusSample = SAMPLE_TEXTURE2D(_CirrusNoiseTex, sampler_CirrusNoiseTex, cirrusUV).rgb;
                            float cr_streaky = cirrusSample.r;
                            float cr_wispy   = cirrusSample.g;
                            float cr_round   = cirrusSample.b;
                            // Coverage 控制
                            float cirrusDensity = remap(cirrusType, 0.5, 1.0, remap(cirrusType, 0.0, 0.5, cr_streaky, cr_wispy), cr_round);
                            cirrusDensity = pow(max(cirrusDensity, 0.0), 1.0 - remap(cirrusCoverage, 0.0, 1.0, -0.9, 0.9));
                            cirrusDensity *= remap(pow(cirrusCoverage, 3.0), 0.0, 0.5, 0.0, 1.0);
                            cirrusDensity = saturate(cirrusDensity);
                            // 光相位-卷云前向散射很强
                            

                            if (cirrusDensity> 0)
                            {                        
                               
                                float cirrusPhase = HenyeyGreenstein(cosTheta, 0.8);
                                if(cirrusPhase < 0) continue;
                                // 光源采样 4 次体积采样
                                float cirrusLightT = sampleCirrusLightTransmittance(cirrusPos, windUVOffset, curlOffset);
                                
                                float3 cirrusLight = mainLight.color.rgb * cirrusDensity * cirrusPhase * cirrusLightT * _CirrusIntensity * _CirrusColor;
                                
                                // 作为 "the last sample" 融入整体的 transmittance
                                scatteredLight += transmittance * cirrusLight;
                            
                                // 人为视线方向的透射率扣减(卷云对后续天空/背景的遮挡)
                                // 视线方向的虚拟步长设为 200.0,与你的光照步进基准保持一致
                                float viewStepSize = 200.0; 
                                float cirrusViewExtinction = cirrusDensity * _CirrusAbsorption;
                                // 扣减全局透射率,让蓝天和星星能被正确挡住
                                transmittance *= exp(-cirrusViewExtinction * viewStepSize);                           
                            }
                        }
                                    
                    }
                }
               
                return float4(scatteredLight, 1.0 - transmittance);          
                
            }
            ENDHLSL
        }
        // Pass 1: Combine
        Pass
        {
            Name "Combine"
            Blend One OneMinusSrcAlpha
            ZTest Always ZWrite Off Cull Off
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            
          
            float4 frag(Varyings input) : SV_Target
            {
                // float4 halfResCloudColor = SAMPLE_TEXTURE2D(_HalfResFogRT, sampler_HalfResFogRT, input.uv);
                // float4 sceneColor = SAMPLE_TEXTURE2D(_SceneColor, sampler_SceneColor, input.uv);
                
                // float3 result = sceneColor * (1.0 - halfResCloudColor.a) +  halfResCloudColor.rgb;
                // return float4(result, 1.0);
                return SAMPLE_TEXTURE2D(_HalfResCloudRT, sampler_HalfResCloudRT, input.uv);

                // ////========= 感知升采样 =============
                // float2 uv = input.uv;
                // float fullResDepth = SampleSceneDepth(uv);   // 全分辨率场景深度(当前像素真实深度)
                // float fullResDepthLinear = LinearEyeDepth(fullResDepth, _ZBufferParams);
                // float2 texelSize = 1.0 / (_ScreenParams.xy * 0.25); // 半分辨率(1/4分辨率)纹素大小,按你的实际降采样比例调整

                // // 4个邻居的UV偏移(左上、右上、左下、右下)
                // float2 offsets[4] = {
                //     float2(-0.5, -0.5), float2(0.5, -0.5),
                //     float2(-0.5,  0.5), float2(0.5,  0.5)
                // };

                // float4 result = 0;
                // float totalWeight = 0;

                // [unroll]
                // for (int i = 0; i < 4; i++)
                // {
                //     float2 sampleUV = uv + offsets[i] * texelSize;
                //     float4 cloudSample = SAMPLE_TEXTURE2D_LOD(_HalfResCloudRT, sampler_HalfResCloudRT, sampleUV, 0);

                //     // 采这个邻居对应的全分辨率深度做对比(用同一个uv位置的全分辨率深度)
                //     float neighborDepth = SampleSceneDepth(sampleUV);
                //     float neighborDepthLinear = LinearEyeDepth(neighborDepth, _ZBufferParams);
                //     float depthDiff = abs(neighborDepthLinear - fullResDepthLinear);
                //     float weight = exp(-depthDiff * 0.1); // 深度差越大权重越低,5000是灵敏度系数,需要按你的深度范围调

                //     result += cloudSample * weight;
                //     totalWeight += weight;
                // }

                // result /= max(totalWeight, 0.0001); // 归一化,防止全部权重为0时除零    
                
                // return result;
            }
            ENDHLSL
        }
  
        //Pass 2: ApplyGroundShadow
        Pass
        {
            Name "ApplyGroundShadow"
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            float _CloudShadowScale;
            
            float4 frag(Varyings input) : SV_Target
            {
                float4 sceneColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv);
                float rawDepth = SampleSceneDepth(input.uv);

                // 天空(云自己)不处理,只压暗有真实深度的物体
                bool isSky = false;
                #if defined(UNITY_REVERSED_Z)
                    if (rawDepth <= 0.00001) isSky = true;
                #else
                    if (rawDepth >= 0.99999) isSky = true;
                #endif
                if (isSky) return sceneColor;

                // 重建世界坐标
                float4 ndc = float4(input.uv * 2.0 - 1.0, rawDepth, 1.0);
                float4 worldPos = mul(_InverseVP, ndc);
                worldPos /= worldPos.w;

                Light mainLight = GetMainLight();
                float3 sunDir = mainLight.direction;

                float layerThickness = _CloudTop - _CloudBottom;
                float2 weatherOffset = _WeatherMoveDirection.xz * _WeatherMoveSpeed * _Time.y;
                float3 windOffset = _WindDirection * _Time.y * _WindSpeed;

                // ===== 3层高度采样,简化路径积分 =====
                const int sampleCount = 3;
                float totalCoverage = 0;
                float totalShapeMod = 0;

                [unroll]
                for (int i = 0; i < sampleCount; i++)
                {
                    float heightFraction = (i + 0.5) / (float)sampleCount; // 0.167, 0.5, 0.833 三层
                    float sampleHeight = _CloudBottom + heightFraction * layerThickness;
                    float heightToSample = sampleHeight - worldPos.y;

                    // 沿太阳方向的水平偏移
                    float2 offset = sunDir.xz * (heightToSample / max(abs(sunDir.y), 0.15));
                    float2 sampleXZ = worldPos.xz + offset;

                    // weatherMap coverage
                    float2 weatherUV = sampleXZ / _WeatherMapScale * _CloudShadowScale + 0.5 + weatherOffset;
                    float coverage = SAMPLE_TEXTURE2D_LOD(_WeatherMap, sampler_WeatherMap, weatherUV, 0).r;
                    totalCoverage += coverage;

                    // ShapeNoise细节调制(粗mip,单次采样)
                    float3 samplePos3D = float3(sampleXZ.x, sampleHeight, sampleXZ.y) + windOffset;
                    float4 shape = SAMPLE_TEXTURE3D_LOD(_ShapeNoiseTex, sampler_ShapeNoiseTex, samplePos3D * _ShapeNoiseScale, 2);
                    float worleyFBM = shape.g * 0.625 + shape.b * 0.25 + shape.a * 0.125;
                    float baseCloud = saturate(worleyFBM * 0.5 + shape.r * 0.5);
                    //float baseCloud = remap(shape.r, worleyFBM - 1.0, 1.0, 0.0, 1.0);
                    totalShapeMod += baseCloud;
                }

                totalCoverage /= sampleCount;
                totalShapeMod /= sampleCount;

                // ShapeNoise在coverage基础上做浮动,增加细节层次和风动感
                float density = totalCoverage * lerp(0.7, 1.3, totalShapeMod);

                float shadowFactor = lerp(1.0, 1.0 - _CloudShadowStrength, saturate(density));
                sceneColor.rgb *= shadowFactor;

                return sceneColor;
            }
            ENDHLSL
        }
        //Pass 2: GodRays
        Pass
        {
            Name "GodRays"
            ZTest Always ZWrite Off Cull Off
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            float4 _SunScreenPos;      // xy=太阳屏幕viewport坐标, z=太阳是否在相机前方(>0代表在前方)
            float _GodRayDensity;
            float _GodRayDecay;
            float _GodRayWeight;
            float _GodRayIntensity;
            int   _GodRaySampleCount;
            float _GodRayObjectContribution;
           
            float4 frag(Varyings input) : SV_Target
            {
                float2 uv = input.uv;
                float4 sceneColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv);
                
                // 太阳在相机背后时,直接不做光柱效果
                if (_SunScreenPos.z <= 0.0) 
                {
                    return sceneColor;
                }
                float2 sunUV = _SunScreenPos.xy;
                float2 noiseUV = uv * _ScreenParams.xy * _BlueNoiseTex_TexelSize.xy;
                float jitter = SAMPLE_TEXTURE2D(_BlueNoiseTex, sampler_BlueNoiseTex, noiseUV * _BlueNoiseTileScale).r;

                float2 deltaUV = (uv - sunUV) * (_GodRayDensity / (float)_GodRaySampleCount);
                float2 samplePos = uv - deltaUV * jitter; // 起点加上抖动偏移,而不是每次都从uv本身开始

                // float2 deltaUV = (uv - sunUV) * (_GodRayDensity / (float)_GodRaySampleCount);
                // float2 samplePos = uv;
                float illuminationDecay = 1.0;
                float3 result = 0;

                [loop]
                for (int i = 0; i < _GodRaySampleCount; i++)
                {
                    samplePos -= deltaUV;

                    // 超出屏幕范围就不再采样,避免边缘拉伸出奇怪的颜色
                    if (samplePos.x < 0.0 || samplePos.x > 1.0 || samplePos.y < 0.0 || samplePos.y > 1.0) break;

                    float sampleDepth = SampleSceneDepth(samplePos);
                    bool sampleIsSky = false;
                    #if defined(UNITY_REVERSED_Z)
                        if (sampleDepth <= 0.00001) sampleIsSky = true;
                    #else
                        if (sampleDepth >= 0.99999) sampleIsSky = true;
                    #endif

                    // 天空/云的采样点权重为1,物体采样点权重按_GodRayObjectContribution削弱
                    float sampleWeight = sampleIsSky ? 1.0 : _GodRayObjectContribution;

                    float3 sceneSample = SAMPLE_TEXTURE2D_LOD(_MainTex, sampler_MainTex, samplePos, 0).rgb;
                    result += sceneSample * illuminationDecay * _GodRayWeight * sampleWeight;
                    illuminationDecay *= _GodRayDecay;
                }

                result *= _GodRayIntensity / (float)_GodRaySampleCount;

                return float4(sceneColor.rgb + result, sceneColor.a);
            }
            ENDHLSL
        }


    }
}

URP 体积云 · 实践合集