shader_type sky; /* =========================================================================== Cartoon Sky · 纯程序化,零贴图 --------------------------------------------------------------------------- 渲染顺序: 渐变天空 → 地面半球 → 星星 → 太阳 → 云层 → 地平线雾霭 所有参数都暴露在 Inspector 里(按分组排列),可以实时调、不用重新编译。 调好的美术取值直接写在下面的 uniform 默认值里 —— 这样即使 .tres 里的 shader_parameter 被清掉,画面也不会跑偏。 =========================================================================== */ // ----------------------------------------------------------- 渐变天空 ----- group_uniforms Gradient; uniform vec3 zenith_color : source_color = vec3(0.090, 0.300, 0.780); // 天顶 uniform vec3 mid_color : source_color = vec3(0.280, 0.600, 0.950); // 中段 uniform vec3 horizon_color : source_color = vec3(0.780, 0.880, 1.000); // 地平线 uniform vec3 ground_color : source_color = vec3(0.290, 0.329, 0.384); // 地面半球 uniform float gradient_bias : hint_range(0.10, 6.00) = 1.40; // 渐变挤压 uniform float band_count : hint_range(0.00, 20.00) = 0.00; // 0=平滑,>0=色阶数 uniform float band_sharpness : hint_range(0.00, 1.00) = 0.90; // 色阶硬度 // ---------------------------------------------------------------- 太阳 ----- group_uniforms Sun; uniform float follow_scene_light : hint_range(0.0, 1.0) = 1.0; // 跟随场景平行光 uniform vec3 sun_direction = vec3(0.35, 0.55, 0.35); // 不跟随时手动指定 uniform vec3 sun_core_color : source_color = vec3(1.000, 0.976, 0.878); uniform vec3 sun_halo_color : source_color = vec3(1.000, 0.827, 0.478); uniform float sun_disk_size : hint_range(0.0, 0.40) = 0.035; // 弧度 uniform float sun_disk_softness : hint_range(0.0, 1.0) = 0.20; uniform float sun_halo_size : hint_range(0.0, 2.00) = 0.55; uniform float sun_halo_strength : hint_range(0.0, 4.00) = 0.65; uniform float sun_bloom_strength : hint_range(0.0, 4.00) = 0.35; uniform float sun_intensity : hint_range(0.0, 8.00) = 1.70; // ---------------------------------------------------------------- 星空 ----- group_uniforms Stars; uniform float star_amount : hint_range(0.0, 1.0) = 0.0; // 太阳落山后自动淡入 uniform float star_density : hint_range(10.0, 160.0) = 60.0; uniform float star_size : hint_range(0.005, 0.30) = 0.050; uniform float star_twinkle : hint_range(0.0, 1.0) = 0.65; uniform vec3 star_color : source_color = vec3(0.85, 0.90, 1.00); // ---------------------------------------------------------------- 云层 ----- group_uniforms Clouds; uniform vec4 cloud_color : source_color = vec4(1.000, 1.000, 1.000, 1.0); // 受光面 uniform vec4 cloud_shadow_color : source_color = vec4(0.450, 0.620, 0.880, 1.0); // 背光面 uniform vec4 cloud_rim_color : source_color = vec4(1.000, 0.930, 0.760, 1.0); // 边缘辉光 uniform float cloud_coverage : hint_range(0.0, 1.0) = 0.55; // 云量 uniform float cloud_scale : hint_range(0.50, 12.0) = 4.0; // 云团大小(越大云越小越多) uniform float cloud_plane_offset : hint_range(0.05, 2.00) = 0.60; // 云层投影软化常数 k uniform float cloud_horizon_fade : hint_range(0.0, 0.60) = 0.08; // 地平线淡出高度(sin 仰角) uniform float cloud_edge : hint_range(0.005, 0.60) = 0.16; // 轮廓软硬 uniform float cloud_warp : hint_range(0.0, 3.0) = 0.30; // 揉皱程度(蓬松感) uniform float cloud_speed : hint_range(0.0, 1.0) = 0.010; // 飘动速度 uniform vec2 cloud_direction = vec2(1.0, 0.25); // 风向 uniform float cloud_shading : hint_range(0.0, 1.0) = 0.55; // 明暗对比 uniform float cloud_rim : hint_range(0.0, 2.0) = 0.40; // 描边辉光强度 uniform float cloud_opacity : hint_range(0.0, 1.0) = 1.0; // ------------------------------------------------------------ 地平线雾霭 --- group_uniforms Haze; uniform vec3 haze_color : source_color = vec3(0.800, 0.890, 1.000); uniform float haze_strength : hint_range(0.0, 1.0) = 0.35; uniform float haze_height : hint_range(0.0, 0.60) = 0.09; // =========================================================================== // 噪声工具 // =========================================================================== float hash21(vec2 p) { p = fract(p * vec2(123.34, 456.21)); p += dot(p, p + 45.32); return fract(p.x * p.y); } // 伪随机梯度方向(每个分量 -1..1)。用 fract 混合而不是 sin, // 大坐标下也不会掉精度。 vec2 hash22(vec2 p) { vec3 p3 = fract(vec3(p.x, p.y, p.x) * vec3(0.1031, 0.1030, 0.0973)); p3 += dot(p3, vec3(p3.y, p3.z, p3.x) + 33.33); return fract(vec2((p3.x + p3.y) * p3.z, (p3.x + p3.z) * p3.y)) * 2.0 - 1.0; } // 梯度噪声(Perlin)。相比 value noise,它在格点处不会出现「梯度为零」的平台, // 等值线是有机的团块 —— 阈值化之后不会切出一块块多边形的棱角。 float gradient_noise(vec2 p) { vec2 i = floor(p); vec2 f = fract(p); // 五次插值:二阶导连续 vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0); float a = dot(hash22(i), f); float b = dot(hash22(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0)); float c = dot(hash22(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0)); float d = dot(hash22(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0)); return mix(mix(a, b, u.x), mix(c, d, u.x), u.y); } // 5 个倍频的分形噪声:每个倍频先旋转再平移,避免出现轴向条纹。 // detail < 1 时高频倍频快速衰减 —— 用于远处的云,防止屏幕频率掉到像素级别以下闪烁。 float fbm(vec2 p, float detail) { float sum = 0.0; float amp = 0.5; float norm = 0.0; mat2 rot = mat2(vec2(0.80, 0.60), vec2(-0.60, 0.80)); for (int i = 0; i < 5; i++) { sum += amp * gradient_noise(p); norm += amp; p = rot * p * 2.03 + 11.31; amp *= 0.5 * detail; } return sum / norm; } // 卡通色阶:把 0..1 压成 n 段硬边色块(n<0.5 时原样返回 = 平滑) float posterize(float t, float n, float sharp) { if (n < 0.5) { return t; } float s = t * n; float base = floor(s); float fr = s - base; float e = mix(0.5, 0.015, clamp(sharp, 0.0, 1.0)); return (base + smoothstep(0.5 - e, 0.5 + e, fr)) / n; } // 云密度场:先做一次廉价的 domain warp 把噪声揉皱,云朵才会蓬松。 // 返回未归一化的原始 fbm —— 调用方需要它来算方向性明暗。 float cloud_field(vec2 p, float warp_amt, float detail) { if (warp_amt > 0.001) { vec2 w = vec2(gradient_noise(p * 0.5 + vec2(13.7, 5.1)), gradient_noise(p * 0.5 + vec2(2.3, 21.9))); p += w * warp_amt; } return fbm(p, detail); } // =========================================================================== // 主函数 // =========================================================================== void sky() { vec3 dir = normalize(EYEDIR); // ---------------------------------------------------------- 渐变天空 -- float up = clamp(dir.y, 0.0, 1.0); float t = pow(up, 1.0 / max(gradient_bias, 0.05)); t = posterize(t, band_count, band_sharpness); vec3 col = mix(horizon_color, mid_color, smoothstep(0.02, 0.30, t)); col = mix(col, zenith_color, smoothstep(0.30, 0.80, t)); // 地平线以下:压向地面色 float below = 1.0 - smoothstep(-0.05, 0.005, dir.y); col = mix(col, ground_color, below * 0.9); // --------------------------------------------------------------- 太阳 -- vec3 sun_dir = normalize(sun_direction); if (LIGHT0_ENABLED && follow_scene_light > 0.5) { sun_dir = normalize(LIGHT0_DIRECTION); } float sun_dot = clamp(dot(dir, sun_dir), -1.0, 1.0); // 小角度近似,比 acos 数值稳定得多 float ang = sqrt(max(2.0 * (1.0 - sun_dot), 0.0)); // 太阳低于地平线时自动入夜 float night = 1.0 - smoothstep(-0.10, 0.08, sun_dir.y); // --------------------------------------------------------------- 星空 -- if (star_amount > 0.001 && night > 0.001 && dir.y > -0.02) { vec2 suv = vec2(atan(dir.x, dir.z), dir.y); vec2 gv = suv * star_density; vec2 id = floor(gv); vec2 f = fract(gv) - 0.5; float rnd = hash21(id); // 稀疏化:只有一小部分格子长星星 float keep = step(0.93, rnd); vec2 offset = (vec2(hash21(id + 3.7), hash21(id + 9.1)) - 0.5) * 0.7; float d = length(f - offset); float twinkle = mix(1.0, 0.30 + 0.70 * sin(TIME * (1.5 + rnd * 4.0) + rnd * 40.0), star_twinkle); float star = (1.0 - smoothstep(0.0, max(star_size, 0.0001), d)) * keep * twinkle; col += star_color * star * star_amount * night * smoothstep(-0.02, 0.10, dir.y); } // --------------------------------------------------------------- 太阳 -- // 硬边圆盘 float disk_in = max(sun_disk_size * (1.0 - sun_disk_softness * 0.85), 0.0001); float disk = 1.0 - smoothstep(disk_in, max(sun_disk_size, disk_in + 0.0001), ang); // 光晕 float halo = pow(1.0 - smoothstep(sun_disk_size, sun_disk_size + max(sun_halo_size, 0.0001), ang), 2.0); // 大范围泛光 float bloom = pow(max(sun_dot, 0.0), 12.0); vec3 sun_col = sun_core_color * (disk * sun_intensity) + sun_halo_color * (halo * sun_halo_strength * sun_intensity) + sun_halo_color * (bloom * sun_bloom_strength); col += sun_col * (1.0 - below); // --------------------------------------------------------------- 云层 -- // 云层投影:uv = dir.xz / (dir.y + k),即把视线投到一张水平云层上, // 但分母里加了个软化常数 k。一次解决四件事: // · 半径有上限 1/k —— 噪声频率不会在地平线处爆炸,不会拉出拖影或摩尔纹 // · 天顶不再是奇点 —— 近天顶处映射退化成局部均匀缩放,云不会朝一点收拢 // · 云的角尺寸从地平线到天顶平滑放大 2~3 倍 —— 保留空气透视;而保角投影 // 整片天的云一样大,看上去像一块贴图的平顶 // · 方位角方向天然无缝 vec2 uv = (dir.xz / max(dir.y + cloud_plane_offset, 0.02)) * cloud_scale; float far = 1.0 - smoothstep(0.06, 0.45, dir.y); // 远处云的屏幕频率更高 —— 按距离砍掉高频倍频,避免闪烁 float detail = mix(1.0, 0.35, far); vec2 wind = cloud_direction; if (length(wind) < 0.0001) { wind = vec2(1.0, 0.0); } uv += normalize(wind) * (TIME * cloud_speed * 12.0); // 形状采样 + 朝太阳方位偏移一次的采样 → 得到方向性明暗 vec2 sun_az = sun_dir.xz; if (length(sun_az) < 0.0001) { sun_az = vec2(1.0, 0.0); } sun_az = normalize(sun_az); vec2 light_off = sun_az * (cloud_shading * 0.8); float raw = cloud_field(uv, cloud_warp, detail); float raw_lit = cloud_field(uv + light_off, cloud_warp, detail); // 原始 fbm 落在 -0.4..0.4 附近,拉开对比后映射到 0..1 再交给阈值 float dens = clamp(raw * 1.3 + 0.5, 0.0, 1.0); // 云量控制阈值:coverage 越大,阈值越低,云越多。 // 远处再抬高一截 —— 低仰角处每单位立体角里的云更多,不抑制会糊成一片「地毯」。 float thresh = mix(0.80, 0.10, cloud_coverage) + far * 0.10; float mask = smoothstep(thresh, thresh + cloud_edge, dens); // 受光判定:朝太阳方向采样到的密度更低 → 当前点处在迎光侧。 // 再按厚度压暗云芯,最后压成 3 段,得到赛璐璐风格的两面明暗。 float lit = clamp((raw - raw_lit) * cloud_shading * 20.0 + 0.5, 0.0, 1.0); float thickness = smoothstep(thresh, thresh + 0.5, dens); lit = clamp(lit - thickness * 0.32, 0.0, 1.0); lit = posterize(lit, 3.0, 0.9); vec3 cloud_col = mix(cloud_shadow_color.rgb, cloud_color.rgb, lit); // 边缘辉光:靠近轮廓、且朝太阳那一侧 float interior = smoothstep(thresh, thresh + cloud_edge * 2.0, dens); float rim = mask * (1.0 - interior) * smoothstep(0.45, 0.95, lit) * cloud_rim; cloud_col += cloud_rim_color.rgb * rim; // 越贴近地平线越淡 float cloud_a = mask * cloud_opacity * smoothstep(0.0, max(cloud_horizon_fade, 0.001), dir.y); col = mix(col, cloud_col, cloud_a); // ----------------------------------------------------------- 地平线雾 -- float haze = pow(1.0 - smoothstep(0.0, max(haze_height, 0.001), abs(dir.y)), 2.0); col = mix(col, haze_color, haze * haze_strength * (1.0 - cloud_a * 0.7)); COLOR = col; }