问题
在做风格化效果时,经常会用到Ramp Tex,但先在PS中生成再转到Unity又太麻烦了。为此本篇展示了一个实时生成Ramp Tex的工具。该工具有以下特性:
- 可以和指定material中的Texture2D属性绑定,生成的Ramp Tex会实时传递给该属性对象
- 在GPU中计算Ramp Tex
- 支持一个Ramp Tex中绘制多个Ramp Color
- 可以手动选择混合、不混合Ramp Tex:由于一个Ramp Tex中可能包含多个Ramp Color,可以手动选择是否它们混合(从上到下对ramp color lerp)
- 支持SRGB模型
- 可实时预览生成的Ramp Tex
- 可以手动控制生成的Ramp Tex的分辨率大小(Tex width)
- 可以把当前对工具的设置保存在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);
}
Comments | NOTHING