【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 仰望星空 [调皮] [机智] 谢谢
页:
[1]