IT圈老男孩1 发表于 2022-7-10 21:36

【Unity工具】使用Shader快速实时生成复合Ramp图

在做卡渲时,使用Ramp图是一个非常常用的着色技巧。合理的使用Ramp图可以对着色进行很多风格化的定制。然而Ramp图本身是一个预计算的结果,如果流程不够好,调制Ramp效果的效率就会变得很差。
生成Ramp图的方式有很多,通常第一想法是选择把数据从Gradient结构中通过Evaluate函数读取信息,然后逐像素用Texture2D.SetPixel函数进行绘制。这种执行方式较为简单,但通常适用于不会做多条混合的Gradient,且SetPixel的运行效率较为低下。对于高分辨率的贴图生成会有明显的卡顿现象(没试过的朋友可以自己实践一下)。
因此为了能够实现实时的Ramp预览,我抛弃了上述方案,选择在Shader内做混合。
使用Shader做混合的核心要点在于传入的数据不是逐像素的,而是直接传入Gradient内的参数,在Shader内进行混合。
数据存储

Gradient数据是以key的方式存储的。Unity API考虑到Alpha的key数量和color的key数量可能不同,因此在一个Gradient结构中使用GradientColorKey结构创建数组以存储颜色信息,再以GradientAlphaKey结构创建数组以单独存储Alpha信息



GradientColorKey与GradientAlphaKey

通过这个结构,发现可以使用一个float3的数组和一个float的数组组合传递一组Gradient的key信息。但由于Shader传入的数组长度是需要确定的值,创建以后不可修改。因此不论我们要同时渲染几条Gradient,我们都需要知道一个Gradient结构最多需要传输多少数据。好在Unity官方规定了这个数组的最大与最小长度。



数组最大长度为8,最小为2

通过API文档得知一个Gradient的Color和Alpha的key数组最大长度都是8个,但是考虑到在实际配置中,可能有如下两头都没有key的情况,为了保证能正确进行混合,我这里将单个Gradient传输的数组长度上限设置为了8+2=10个



两头没有Key的情况

数据传递

由于Shader中数组长度固定,如果要做多条Gradient的混合,为了实现数据的正确混合,在实际进行数组传输的过程中每个Gradient都需要分配满长度为10的数组。因此在设置数据数组的过程中,我除了拷贝数据以外做了两个额外操作。
一是分别检测每个Gradient内Color数组的第一个数据的time。如果第一个数据的time不是0,说明出现了上图左侧端点没有key的配置。此时需要先塞入一个和第一个key的颜色数据一样但time=0的新key,再遍历Gradient数据进行拷贝。
二是如果Gradient数据不满10个,需要将后续预留空缺的数据使用最后一个key的颜色数据但time=1进行填充,以确保传输到shader的每个Gradient的color数组都有十个数据,以便在shader中遍历各个gradient进行混合时能够产生等长的数组偏移。
所有Gradient数据被塞入到一个超长的array后再统一传输给Shader。我这里默认一次最多混合8条Gradient,因此数组长度设置为80。这里没有考虑Ramp使用Alpha的情况,仅对color数据进行了传递。每个color数据需要有其对应的time信息才能在shader内正确混合,因此传输的数组除了存储color的数组外,还要传输一个等长且一一对应的time数组
private void SetGradient()
{
    _tempColor = new Color;
    _tempPoint = new float;
    //逐条遍历赋值
    for (int i = 0; i < _ribbons.Count; i++)
    {
      SetGradientToArray(_ribbons, i);
    }
    //实际生效的Gradient数量
    _previewMat.SetFloat(REAL_NUM, _ribbons.Count);
    //所有Gradient的GradientColorKey[]数据拼接
    _previewMat.SetColorArray(COLOR_ARRAY, _tempColor);
    //所有Gradient的GradientAlphaKey[]数据拼接
    _previewMat.SetFloatArray(POINT_ARRAY, _tempPoint);
}

private void SetGradientToArray(Gradient source, int serial)
{
    int count = source.colorKeys.Length;
    int offset = 0;
    int outsideOffset = serial * 10;
    for (int i = 0; i < 10; i++)
    {
      //检测color第一个数据是不是在最左端,如果不是就添加一个
      if (i == 0 && source.colorKeys.time != 0)
      {
            _tempColor = source.colorKeys.color;
            _tempPoint = 0;
            offset = -1;
            continue;
      }
      //固定依次赋值10个数据,超过Gradient内数据数量就重复设置最后一个color值在time=1的位置
      if (i + offset < count)
      {
            _tempColor = source.colorKeys.color;
            _tempPoint = source.colorKeys.time;
      }
      else
      {
            _tempColor = source.colorKeys.color;
            _tempPoint = 1;
      }
    }
}
Shader计算

接下来就是在Shader里计算混合,分横向单条Gradient的混合和纵向多条Gradient的混合

[*]单条Gradient的混合
选取到正确的Gradient与Time值后,对Time数组进行遍历,找到第一个time大于输入值的序号就是混合右侧点的序号,它的前一个就是混合左侧点的序号。再使用两个点的time值做linearstep计算,得到单条Gradient内的混合结果。
float4 GetGradient(int num, float u)
{
    int i = 0;
    int left = 0;
    int right = 7;
    UNITY_UNROLL
    for (i = 0; i < 8; i++)
    {
      if(_PointArray >= u)
      {
            right = i;
            break;
      }
    }
    left = max(0, right - 1);
    return lerp(_ColorArray, _ColorArray, linearstep(u, _PointArray, _PointArray));
}2. 多条Gradient的混合
多组Gradient在同一张Texture上显示时,分为混合和不混合两种方式。贴图上有n组不混合的Gradient,则单组Gradient的高度为1/n(uv单位);如果有n组混合的Gradient,则有n-1组混合组,单组高度为1/(n-1)(uv单位) (比如顶底两条Gradient之间只存在一段混合,上中下三条Gradient只存在上中和中下两组混合)



左侧为不混合,右侧为混合


[*]不混合的情况,单条Gradient高度为1/n,则读取uv中的y值除以1/n,向下取整就是需要读取的Gradient数据的序号:以上图3条Gradient为例,y = 0.7时,单条Gradient高度为1/3 ≈ 0.3333,y/(1/n) ≈ 2.10。即当前需要读取序号为2的Gradient的数据
[*]混合的情况,仍然以上述3条Gradient、y=0.7为例,可知n-1=2, y/(1/(n-1)) = 1.4。即当前需要混合序号1与序号2的Gradient值,混合比例为(1.4-1) : (2-1.4) = 2 : 3
#ifdef _LERP_MODE
if(_RealNum>1) bandwidth = 1 / (_RealNum - 1);
#endif
float y = 1 - i.uv.y;
#ifdef _LERP_MODE
return lerp(GetGradient(floor(y/bandwidth), i.uv.x), GetGradient(ceil(y/bandwidth), i.uv.x), frac(y/bandwidth));
#else
return GetGradient(floor(y/bandwidth), i.uv.x); 最后在C#脚本中使用上述shader对应的材质球与Graphic.Blit()函数将结果渲染到一张RT上,存储赋值这个RT就可以实时应用Ramp的效果。
我做了一套开源工具,可以直接查看使用和接入,shader基于URP的环境,readme有空补(懒
https://github.com/Reguluz/MFRampMaker.git

kyuskoj 发表于 2022-7-10 21:40

仰望星空

johnsoncodehk 发表于 2022-7-10 21:40

[调皮]

JamesB 发表于 2022-7-10 21:43

[机智]

ainatipen 发表于 2022-7-10 21:45

谢谢
页: [1]
查看完整版本: 【Unity工具】使用Shader快速实时生成复合Ramp图