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的范围来判断,但是球壳范围判断会复杂一些 射线上的某个点的表达式是:
那么假设点打在了球壳上,那么就会有:
- C是表示球心
- r则是半径
- D是步进单位
- t是步进次数
球壳上的点到球心的距离是r,也就是半径,根据这个限定条件,就可以求出t
因为原点O表示相机位置,C是球心位置,这里可以先进行简化,直接可以合并成相机到球心的距离
然后就是展开化简
然后移项,这样就得到了关于t的
这里因为D是单位向量,所以平方之后直接是1了,现在的形式和下面的形式类似,那么就是求方程解
最后t的表达式就是,这里需要限定b² − c > 0
这里把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
}
}
}