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

文章目录

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

0. 效果预览

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


1. 原理简述

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

标准 Lambert 着色的流程是:

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

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

hlsl 复制代码
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(可直接用)

hlsl 复制代码
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. 参数说明

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

6. 变体与扩展

6.1 卡通二分着色(Cel Shading)

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

hlsl 复制代码
// 方法 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 查表:

hlsl 复制代码
// 在 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 用另一个参数(如粗糙度、海拔、生命值百分比等):

hlsl 复制代码
// 例: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 和一次贴图采样,低端手机也能流畅运行。
相关推荐
WiChP1 天前
【V0.1B6】从零开始的2D游戏引擎开发之路
java·log4j·游戏引擎
小拉达不是臭老鼠1 天前
Unity05_3D数学
学习·unity·游戏引擎
梵尔纳多1 天前
OpenGL 骨骼动画
c++·图形渲染·opengl
charlie1145141911 天前
通用GUI编程技术——图形渲染实战(三十一)——Direct2D效果与图层:高斯模糊到毛玻璃
c++·图形渲染·gui·win32
风酥糖1 天前
Godot游戏练习01-第28节-显示效果与音效
游戏·游戏引擎·godot
不过如此19511 天前
pyinstaller打包GUI项目实践
windows·python·ui
油炸自行车1 天前
Unity URDF 导入后运行报错问题笔记
笔记·unity·游戏引擎·数字孪生·urdf·工业仿真·虚拟与现实
南無忘码至尊1 天前
Unity学习90天 - 第 5 天 - 阶段小项目
学习·unity·c#·游戏引擎
郝学胜-神的一滴1 天前
中级OpenGL教程 001:从Main函数到相机操控的完整实现
c++·程序人生·unity·图形渲染·unreal engine·opengl
RReality1 天前
【Unity Shader URP】顶点波浪动画(Vertex Wave)实战教程
ui·unity·游戏引擎·图形渲染