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

【Unity Shader URP】色带渐变着色(Ramp Shading)实战教程

文章目录

    • 0. 效果预览
    • 1. 原理简述
    • 2. 功能点
    • 3. 完整 Shader(可直接用)
    • 4. 使用方法
    • 5. 参数说明
    • 6. 变体与扩展
      • 6.1 卡通二分着色(Cel Shading)
      • 6.2 多光源 Ramp
      • 6.3 2D Ramp 贴图(多条件查表)
    • 7. 常见问题
    • 8. 性能建议

0. 效果预览

色带渐变着色(Ramp Shading)是最灵活的非真实感光照技巧之一:把 NdotL 当"索引",用一张 1D 渐变贴图查表,让光影过渡完全由美术控制。卡通赛璐珞硬切、冷暖色调渐变、金属质感色带——换张贴图就换一种风格,代码不用动。


1. 原理简述

Ramp Shading 的本质:把 Lambert 光照的连续灰度值,映射到一张渐变贴图上查色,用贴图颜色代替数学公式决定明暗过渡。

标准 Lambert 着色的流程是:

float NdotL = dot(normalWS, lightDirWS); // -1 ~ 1 float halfLambert = NdotL * 0.5 + 0.5; // 映射到 0 ~ 1 finalColor = baseColor * halfLambert; // 连续灰度过渡

Ramp Shading 只改了最后一步——不直接用halfLambert乘颜色,而是拿它当 UV.x 去采样一张渐变贴图:

float2 rampUV = float2(halfLambert, 0.5); // halfLambert 做横轴坐标 half3 rampColor = SAMPLE_TEXTURE2D(rampTex, sampler_rampTex, rampUV).rgb; finalColor = baseColor * rampColor; // 贴图颜色决定明暗

贴图的左端对应暗部(NdotL ≈ 0),右端对应亮部(NdotL ≈ 1)。贴图里画什么色阶,渲染就出什么过渡——硬切两色就是卡通,平滑渐变就接近 PBR,中间加一道亮色就是金属高光带。


2. 功能点

  • 1D Ramp 查表着色:用 Half-Lambert 值采样渐变贴图,光影过渡完全由贴图控制
  • Half-Lambert 映射:NdotL 重映射到 0~1,消除背光面全黑问题
  • 主贴图叠乘:保留模型原始纹理细节,Ramp 只控制明暗色调
  • Ramp 贴图可热替换:Inspector 里换一张渐变图就换一种风格,无需改代码
  • 环境光补偿:叠加一层环境色,避免暗部纯黑
  • GPU Instancing:支持多实例合批渲染

3. 完整 Shader(可直接用)

Shader "Custom/RampShading_URP" { Properties { // 主贴图(模型漫反射纹理) _BaseMap ("Base Map", 2D) = "white" {} // 主颜色叠乘 _BaseColor ("Base Color", Color) = (1,1,1,1) // Ramp 渐变贴图(1D 查表,左=暗 右=亮) _RampTex ("Ramp Texture", 2D) = "white" {} // Ramp 影响强度(0=纯 Lambert,1=完全 Ramp 查表) _RampStrength ("Ramp Strength", Range(0, 1)) = 1.0 // 环境光颜色(补偿暗部,避免全黑) _AmbientColor ("Ambient Color", Color) = (0.1, 0.1, 0.15, 1) } SubShader { Tags { "RenderPipeline" = "UniversalRenderPipeline" "Queue" = "Geometry" "RenderType" = "Opaque" } Pass { Name "RampShadingPass" Tags { "LightMode" = "UniversalForward" } Cull Back ZWrite On Blend Off HLSLPROGRAM #pragma vertex vert #pragma fragment frag // GPU Instancing 支持 #pragma multi_compile_instancing #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" // ========================================================= // 贴图声明 // ========================================================= TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); TEXTURE2D(_RampTex); SAMPLER(sampler_RampTex); // ========================================================= // 材质属性(与 Properties 一一对应) // ========================================================= float4 _BaseMap_ST; float4 _BaseColor; float4 _RampTex_ST; float _RampStrength; float4 _AmbientColor; struct Attributes { float4 positionOS : POSITION; // 模型空间顶点 float3 normalOS : NORMAL; // 模型空间法线 float2 uv : TEXCOORD0; // UV 坐标 UNITY_VERTEX_INPUT_INSTANCE_ID }; struct Varyings { float4 positionHCS : SV_POSITION; // 裁剪空间位置 float2 uv : TEXCOORD0; // 传递 UV float3 normalWS : TEXCOORD1; // 世界空间法线 UNITY_VERTEX_INPUT_INSTANCE_ID UNITY_VERTEX_OUTPUT_STEREO }; // ========================================================= // 顶点着色器:变换位置 + 传递世界空间法线 // ========================================================= Varyings vert(Attributes v) { Varyings o; UNITY_SETUP_INSTANCE_ID(v); UNITY_TRANSFER_INSTANCE_ID(v, o); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); // 模型空间 → 裁剪空间 o.positionHCS = TransformObjectToHClip(v.positionOS.xyz); // UV 变换(支持 Tiling / Offset) o.uv = TRANSFORM_TEX(v.uv, _BaseMap); // 模型空间法线 → 世界空间法线 o.normalWS = TransformObjectToWorldNormal(v.normalOS); return o; } // ========================================================= // 片元着色器:Half-Lambert → Ramp 查表 → 最终着色 // ========================================================= half4 frag(Varyings i) : SV_Target { UNITY_SETUP_INSTANCE_ID(i); // ===== 1) 采样主贴图 ===== half4 baseCol = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv); baseCol *= (half4)_BaseColor; // ===== 2) 获取主光源方向 ===== Light mainLight = GetMainLight(); float3 lightDirWS = normalize(mainLight.direction); // ===== 3) 计算 Half-Lambert ===== float3 normalWS = normalize(i.normalWS); // NdotL:法线·光照方向,范围 -1 ~ 1 float NdotL = dot(normalWS, lightDirWS); // Half-Lambert:映射到 0 ~ 1,消除背光面全黑 float halfLambert = NdotL * 0.5 + 0.5; // ===== 4) Ramp 贴图查表 ===== // 用 halfLambert 做 UV.x,0.5 做 UV.y(取贴图中间行) float2 rampUV = float2(halfLambert, 0.5); half3 rampColor = SAMPLE_TEXTURE2D(_RampTex, sampler_RampTex, rampUV).rgb; // ===== 5) 混合最终颜色 ===== // _RampStrength 控制 Ramp 查表和普通 Lambert 的混合比例 half3 lambert = (half3)(halfLambert * mainLight.color); half3 lighting = lerp(lambert, rampColor * mainLight.color, _RampStrength); // 叠加环境光补偿 lighting += _AmbientColor.rgb; // 基础色 × 光照结果 half3 finalColor = baseCol.rgb * lighting; return half4(finalColor, baseCol.a); } ENDHLSL } } }

4. 使用方法

  1. 在 Unity 项目的Assets/Shaders/下新建文件RampShading_URP.shader,粘贴上方完整代码。

  2. 新建材质(Create → Material),Shader 选择Custom/RampShading_URP

  3. 准备 Ramp 渐变贴图:

    • 打开 Photoshop / GIMP / 任意绘图工具
    • 新建一张256×1的图片(宽 256 像素,高 1 像素)
    • 从左到右画渐变:左端=暗部颜色,右端=亮部颜色
    • 导出为 PNG,导入 Unity
  4. 关键:Ramp 贴图的 Import Settings(不设会糊掉):

    • Wrap ModeClamp(防止边缘采样到对面的颜色)
    • Filter ModePoint(硬切风格)或Bilinear(平滑渐变风格)
    • 关闭Generate Mip Maps(1D 查表不需要 Mip)

  1. 将材质赋给场景中的模型(球体、角色模型效果最明显)。

  2. 在 Inspector 中配置参数:

    • Base Map:模型的漫反射贴图
    • Ramp Texture:拖入刚才做的渐变贴图
    • Ramp Strength:1.0 = 完全 Ramp 查表,0.5 可以和 Lambert 混合
    • Ambient Color:暗部补色,推荐偏冷的深蓝(0.05, 0.05, 0.1)

  1. 在场景中旋转光源方向,观察明暗分界处的色带变化——这就是 Ramp Shading 的核心视觉效果。

5. 参数说明

参数类型范围/默认值说明
_BaseMap2Dwhite模型主贴图(漫反射纹理)
_BaseColorColor(1,1,1,1)主颜色叠乘
_RampTex2DwhiteRamp 渐变贴图,左=暗 右=亮,建议 256×1
_RampStrengthRange(0,1)1.0Ramp 查表强度:0=纯 Lambert,1=完全 Ramp
_AmbientColorColor(0.1,0.1,0.15,1)环境光补偿色,避免暗部纯黑

6. 变体与扩展

6.1 卡通二分着色(Cel Shading)

最简单的卡通风格——亮面和暗面只有两个色阶,硬切分界:

// 方法 A:用 step 替代 Ramp 贴图(不需要额外贴图) half3 shadow = half3(0.4, 0.3, 0.5); // 暗部色(偏紫) half3 lit = half3(1.0, 0.95, 0.9); // 亮部色(偏暖) float cel = step(0.5, halfLambert); // halfLambert >= 0.5 ? 1 : 0 half3 rampColor = lerp(shadow, lit, cel);

也可以用smoothstep(0.48, 0.52, halfLambert)做一个极窄的软过渡,避免锯齿。

6.2 多光源 Ramp

当前代码只用了主光源。如果需要额外光源(点光源等)也走 Ramp 查表:

// 在 frag 中,主光源计算之后追加 uint additionalLightCount = GetAdditionalLightsCount(); for (uint idx = 0; idx < additionalLightCount; idx++) { Light addLight = GetAdditionalLight(idx, positionWS); float addNdotL = dot(normalWS, normalize(addLight.direction)); float addHalf = addNdotL * 0.5 + 0.5; half3 addRamp = SAMPLE_TEXTURE2D(_RampTex, sampler_RampTex, float2(addHalf, 0.5)).rgb; // 乘以光源颜色和衰减 lighting += addRamp * addLight.color * addLight.distanceAttenuation; }

需要在 Varyings 中传递positionWS,并在顶点着色器中计算。

6.3 2D Ramp 贴图(多条件查表)

用一张256×N的 2D Ramp 贴图,UV.x 仍用 halfLambert,UV.y 用另一个参数(如粗糙度、海拔、生命值百分比等):

// 例:UV.y 用材质的 _RampRow 参数选择 Ramp 行 float2 rampUV = float2(halfLambert, _RampRow); half3 rampColor = SAMPLE_TEXTURE2D(_RampTex, sampler_RampTex, rampUV).rgb;

这样一张贴图就能存多种光影风格,运行时通过_RampRow切换。


7. 常见问题

Q: Ramp 贴图效果和预期不一致,明暗分界位置偏了?
A: 检查贴图的Wrap Mode是否设为Clamp。如果是 Repeat,halfLambert 在 0 和 1 附近会采样到贴图另一端的颜色,导致边缘出现错误色带。

Q: 想要硬切卡通风格,但过渡还是有模糊?
A: 把 Ramp 贴图的Filter Mode改为Point (no filter)。Bilinear/Trilinear 会在相邻像素之间插值,把硬切的色阶边界平滑掉。

Q: 模型背光面完全是 Ramp 贴图最左端的颜色,太暗/太脏?
A: 两个解法:① 调大_AmbientColor补亮暗部;② 修改 Ramp 贴图左端的颜色,让暗部也有明确的色调(比如冷紫色、深蓝色),不要用纯黑。

Q: 场景里没有灯光,模型全黑?
A: 这个 Shader 依赖场景主光源(GetMainLight())计算 NdotL。确保场景中有一个 Directional Light,并且 URP 的 Lighting 设置中主光源没有被禁用。

Q: Ramp 贴图需要很高分辨率吗?
A: 不需要。Ramp 本质是一维查表,256×1 完全够用。即使做 2D Ramp(多行),256×16 也绰绰有余。文件极小,运行时采样开销可忽略。


8. 性能建议

  • 采样开销极低:Ramp 查表只增加一次 texture fetch(256×1 贴图常驻缓存),对帧率几乎无影响。
  • 关闭 Mip Maps:Ramp 贴图是 1D 查表,生成 Mip 没有意义,反而会在远处模糊掉色阶边界。Import Settings 中关掉Generate Mip Maps
  • Clamp 模式必选:Wrap Mode 设为 Clamp 不只是视觉正确性问题,也避免了边缘采样穿越到另一端带来的额外缓存未命中。
  • 合批友好:单 Pass Opaque Shader,不影响 SRP Batcher。如果多个物体共用同一张 Ramp 贴图和材质,可以正常合批。如果需要不同风格但想保持合批,用 2D Ramp +MaterialPropertyBlock设置不同的_RampRow
  • 移动端安全:整个 Shader 没有复杂数学运算,最重的计算就是一个dot和一次贴图采样,低端手机也能流畅运行。
http://www.jsqmd.com/news/645079/

相关文章:

  • 终极指南:如何用DouyinLiveRecorder轻松录制40+平台直播内容
  • 传输对象中的数据封装与网络传输
  • 从无线电到栅栏密码:一次完整的CTF杂项题逆向实战(含RX-SSTV配置+音频样本)
  • C#怎么使用Switch表达式 C#新版switch表达式和传统switch语句的区别和升级写法【语法】
  • Qt5实战:用QTableView实现高效分页(附完整源码)
  • 比chmod更灵活!Ubuntu下setfacl的7个高阶用法(附真实案例)
  • MTK芯片Android 8.1设备获取完整root权限的5个关键步骤(附实测避坑指南)
  • IEEE LaTeX模板引用格式总调不对?可能是你的.bib文件多了这些“垃圾”字段
  • 2025网盘直链下载神器:八大平台高速下载完整指南
  • ChatGPT+图表狐:5分钟搞定深度学习Loss曲线可视化(附实战截图)
  • STM32F4 ADC初始化实战:从零开始配置模数转换器
  • Bootstrap5 滚动监听
  • 罗技鼠标宏压枪终极指南:3分钟快速上手绝地求生自动压枪
  • Bilibili视频解析终极指南:三步快速上手免费API工具
  • 深入解析:Flutter 项目结构该如何设计,才能支撑长期迭代
  • 文档密码破解工具
  • 算法训练营第二天| 27.移除元素
  • 探索前沿技术趋势:2023年最值得关注的五大创新领域
  • C语言的循环语句
  • netDxf终极指南:.NET开发者的CAD文件处理神器
  • SAP BSP网页端开发实战:从SE80到MVC架构的完整指南
  • 无实体公司在美国如何雇人?一文读懂Safeguard Global名义雇主EOR服务 - 品牌2026
  • LIN总线开发避坑指南:用LDF Tool处理NAD分配与信号编码的5个关键细节
  • 企业智能体安全管理:从开源探索到企业级落地的必经之路 - 品牌2026
  • 高效数据处理 | 利用EXCEL插件实现度分秒与弧度、度的快速互转
  • 胡桃工具箱终极指南:免费开源原神助手如何提升你的游戏体验
  • 2025届必备的降AI率方案实际效果
  • VC++ 打造小型HTTP服务器
  • 终极指南:如何用novideo_srgb实现硬件级显示器色彩校准,解决宽色域显示器色彩过饱和问题
  • GetQzonehistory:你的QQ空间记忆守护者,永久保存青春时光