找回密码
 立即注册
查看: 322|回复: 4

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

[复制链接]
发表于 2022-7-10 21:36 | 显示全部楼层 |阅读模式
在做卡渲时,使用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[80];
    _tempPoint = new float[80];
    //逐条遍历赋值
    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[0].time != 0)
        {
            _tempColor[outsideOffset] = source.colorKeys[0].color;
            _tempPoint[outsideOffset] = 0;
            offset = -1;
            continue;
        }
        //固定依次赋值10个数据,超过Gradient内数据数量就重复设置最后一个color值在time=1的位置
        if (i + offset < count)
        {
            _tempColor[outsideOffset + i] = source.colorKeys[i + offset].color;
            _tempPoint[outsideOffset + i] = source.colorKeys[i + offset].time;
        }
        else
        {
            _tempColor[outsideOffset + i] = source.colorKeys[count - 1].color;
            _tempPoint[outsideOffset + i] = 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[num * 10 + i] >= u)
        {
            right = i;
            break;
        }
    }
    left = max(0, right - 1);
    return lerp(_ColorArray[num * 10 + left], _ColorArray[num * 10 + right], linearstep(u, _PointArray[num * 10 + left], _PointArray[num * 10 + right]));
}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

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2022-7-10 21:40 | 显示全部楼层
仰望星空
发表于 2022-7-10 21:40 | 显示全部楼层
[调皮]
发表于 2022-7-10 21:43 | 显示全部楼层
[机智]
发表于 2022-7-10 21:45 | 显示全部楼层
谢谢
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-15 16:45 , Processed in 0.091444 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表