问题

在做风格化效果时,经常会用到Ramp Tex,但先在PS中生成再转到Unity又太麻烦了。为此本篇展示了一个实时生成Ramp Tex的工具。该工具有以下特性:

  1. 可以和指定material中的Texture2D属性绑定,生成的Ramp Tex会实时传递给该属性对象
  2. 在GPU中计算Ramp Tex
  3. 支持一个Ramp Tex中绘制多个Ramp Color
  4. 可以手动选择混合、不混合Ramp Tex:由于一个Ramp Tex中可能包含多个Ramp Color,可以手动选择是否它们混合(从上到下对ramp color lerp)
  5. 支持SRGB模型
  6. 可实时预览生成的Ramp Tex
  7. 可以手动控制生成的Ramp Tex的分辨率大小(Tex width)
  8. 可以把当前对工具的设置保存在unity中,且下次重新打开可以读取上次保存的设置

实现

绑定Material及其中的Texture2D

  • 为了方便,这里采用了右键Material面板的方式来绑定Material

    如下图中点击"Generate Ramp Tex"来启动工具并绑定Material

    [MenuItem("CONTEXT/Material/Elysia/Generate Ramp Tex", priority = 0)]
    public static void MatShowWindow(MenuCommand menuCommand)
    {
      if (Ins == null)
      {
          Ins = GetWindow<RampWindow>();
          Ins.InitData();   // 初始化必要数据
          Ins.Show();       // 显示window
      }
    
      // 绑定material
      Ins.targetMaterial = menuCommand.context as Material;
      Ins.UpdateProperty(); // 获得material的tex Property
    }
    
  • 由于一个Material中很有可能存在多张Texture 2D,因此需要提供一个下拉框来选择需要的Texture 2D对象

    _targetPropertyIndex = EditorGUILayout.Popup("Target Ramp Tex", _targetPropertyIndex, _texNames.ToArray());   // 生成下拉表
    propertyName = _texNames[_targetPropertyIndex];   // 获得指定的property对象
    
  • 最后提供一个按钮,可以手动选择是否和指定Texture 2D连接,实时地将生成的Ramp Tex传过去

    if (GUILayout.Button(_isLinked ? "Break Link" : "Link Target Texture2D"))
    {
      if (_isLinked)
      {
          DestoryLink();
      }
      else
      {
          StarLink();
      }
    }
    
    void StarLink()
    {
      _isLinked = true;
      _oldTex = targetMaterial.GetTexture(propertyName) as Texture2D;
    }
    
    void DestoryLink()
    {
    _isLinked = false;
    }
    

传递Gradient

为了提高速率,本篇选择将Gradient从CPU传递到GPU计算

unity提供了现成的Gradient,主要由color和alpha构成。因此,在传递时,需要传递color array、alpha array

这个Gradient的数量是有限制的,每个gradient的color、alpha的个数上限都是8,但因为可能存在key不在0、不在1的情况,所以需要提供10个key

/// <summary>
/// 为material传递所有gradient
/// </summary>
void SetGradient()
{
    // 最多8个Gradient
    _tempColor = new Color[80];
    _tempPoint = new float[80];

    // 设置每个Gradient
    for (var i = 0; i < _ribbons.Count; ++i)
    {
        SetGradientArray(_ribbons[i], i);
    }

    // 传递给GPU
    _previewMat.SetColorArray(_Color_Array, _tempColor);
    _previewMat.SetFloatArray(_Point_Array, _tempPoint);
    _previewMat.SetFloat(_Real_Num, _ribbons.Count);
}

因为每个Gradient最少存在2个keys,而本篇设定的每个Array最大容量为10,所以需要对剩余的地方进行填补(说的是8个Array,但传递给GPU的是把8个Array合并后的Array

/// <summary>
/// 处理每个gradient,随后传递给shader
/// </summary>
/// <param name="source"> 待处理的gradient </param>
/// <param name="index"> 待处理的gradient的索引 </param>
void SetGradientArray(Gradient source, int index)
{
    // unity 提供的gradient,每个最多有8个keys
    // 但因为可能存在keys不在0 和 1的情况,所以设置keys的最大个数为10
    var gradientOffset = index * 10;
    var length = source.colorKeys.Length;   // 至少存在两个keys

    for (var i = 0; i < 10; ++i)
    {
        // time不在0处,且为开头时,不对它之前的color blend
        if (i == 0 && source.colorKeys[0].time != 0)
        {
            _tempColor[gradientOffset] = source.colorKeys[0].color;
            _tempPoint[gradientOffset] = 0;
            continue;
        }

        if (i < length)
        {
            _tempColor[gradientOffset + i] = source.colorKeys[i].color;
            _tempPoint[gradientOffset + i] = source.colorKeys[i].time;
        }
        else
        {
            // 超出部分取gradient的末尾keys
            _tempColor[gradientOffset + i] = source.colorKeys[length - 1].color;
            _tempPoint[gradientOffset + i] = 1;
        }
    }
}

Lerp、sRGB模式

为了可以手动选择是否启用对应模式,需要写GUI,并视情况是否启用keywords

lerpMode = EditorGUILayout.ToggleLeft("Mix Ramp Tex", lerpMode);
gammaMode = EditorGUILayout.ToggleLeft("sRGB", gammaMode);

// 设置key words
if (_previewMat != null)
{
    if (lerpMode == true)
    {
        _previewMat.EnableKeyword(_Lerp_Mode);
    }
    else if(lerpMode == false)
    {
        _previewMat.DisableKeyword(_Lerp_Mode);
    }

    if (gammaMode == true)
    {
        _previewMat.EnableKeyword(_Gamma_Mode);
    }
    else if(gammaMode == false)
    {
        _previewMat.DisableKeyword(_Gamma_Mode);
    }
}

GPU计算多个Color,并从上到下lerp

为了让一个Ramp Tex中能绘制多个Ramp Color,并能对它们lerp,需要在shader中实现如何对传递到GPU的Gradient lerp

可行的方案:基于uv.y lerp,先横向lerp,再纵向lerp

/// <summary>
/// 计算单个gradient的颜色值
/// 在单个gradient中存在多个time,需要从左到右依次计算time,并依次根据time进行lerp
/// </summary>
/// <param name="num"> gradient index </param>
/// <param name="u"> uv.u </param>
float4 GetSingleGradient(float num, float u)
{
    int i = 0;
    int l = 0, r = 7;

    UNITY_UNROLL
    for(i = 0; i < 8; ++i)
    {
        // 找到time,且该time为lerp的最右端
        if(_PointArray[num * 10 + i] >= u)
        {
            r = i;
            break;
        }
    }
    l = max(0, r - 1);

    float4 resultColor = lerp(_ColorArray[num * 10 + l], _ColorArray[num * 10 + r],
        RemapRange(u, _PointArray[num * 10 + l], _PointArray[num * 10 + r], 0, 1));

    #if defined (_GAMMA_MODE)
        return pow(resultColor, 2.2f);
    #else
        return resultColor;
    #endif
}

PSOutput PS(PSInput i)
{
    PSOutput o;

    int nums = _GradientNums;
    #if defined(_LERP_MODE)
        if(_GradientNums > 1)
        {
            --nums;
        }
    #endif

    float level = i.uv.y * nums;
    #if defined (_LERP_MODE)
        int next = ceil(level);
        float balance = frac(level);

        o.color = lerp(GetSingleGradient(floor(level), i.uv.x), GetSingleGradient(next, i.uv.x), saturate(balance));
    #else
        o.color = GetSingleGradient(floor(level), i.uv.x);
    #endif

    return o;
}

实时预览

只需把计算得到的Ramp Tex传递回来,并展示即可

if (_RT != null && _RT.IsCreated())
{
    var rect = EditorGUILayout.GetControlRect(true, 200);
    rect.width = 200;
    EditorGUI.DrawPreviewTexture(rect, _RT);
}

最终效果

项目链接


他们曾如此骄傲的活过,贯彻始终