
渐变纹理算是在偏卡通风格的渲染里常用的一种套路。其最常见的用法是,利用半兰伯特光照模型计算得到的灰度对渐变纹理进行采样,以达到一种基于模型的明暗进行渐变映射的效果。


本文实现了一个动态生成渐变纹理的小工具(需要挂到物体上使用)。其具有梯度滑块与曲线调整两种模式,可以关联材质,对修改进行实时显示,并且可以导出图像文件。

因为算法没有涉及什么数学过程,实在没什么好讲的,所以直接上代码。个人认为必要的注释也已经写明,如果还有不清楚的可以查官方API文档or留言。
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
using System.IO;
#endif
public class RampTexCreator : MonoBehaviour
{
public Material dstMat;//目标材质
public Gradient gradient = new Gradient();//梯度渐变
public AnimationCurve[] curveRGBA = new AnimationCurve[4];//曲线渐变
public bool curveMode = false;//操作模式
public bool isLinear = true;//颜色是否是线性空间
public int textureIdx = 0;//纹理ID
private void Start()
{
SetTexture();
}
//创建并传递纹理到材质
public void SetTexture()
{
dstMat.SetTexture(
dstMat.GetTexturePropertyNames()[textureIdx],
curveMode ? CreateRampTex(isLinear, curveRGBA) : CreateRampTex(isLinear, gradient)
);
}
//创建纹理
public static Texture2D CreateRampTex(bool isLinear, Gradient gradientIN)
{
Texture2D outTex = new Texture2D(256, 1, TextureFormat.RGBA32, false, isLinear);
for (int i = 0; i < outTex.width; i++)
{
outTex.SetPixel(i, 0, gradientIN.Evaluate(i / (outTex.width - 1.0f)));
}
outTex.wrapMode = TextureWrapMode.Clamp;
outTex.Apply();
return outTex;
}
public static Texture2D CreateRampTex(bool isLinear, params AnimationCurve[] curveIN)
{
Texture2D outTex = new Texture2D(256, 1, TextureFormat.RGBA32, false, isLinear);
for (int i = 0; i < outTex.width; i++)
{
float currentTime = i / (outTex.width - 1.0f);
outTex.SetPixel(i, 0, new Color(
curveIN[0].Evaluate(currentTime),
curveIN[1 < curveIN.Length ? 1 : curveIN.Length - 1].Evaluate(currentTime),
curveIN[2 < curveIN.Length ? 2 : curveIN.Length - 1].Evaluate(currentTime),
curveIN[3 < curveIN.Length ? 3 : curveIN.Length - 1].Evaluate(currentTime)
));
}
outTex.wrapMode = TextureWrapMode.Clamp;
outTex.Apply();
return outTex;
}
}
#if UNITY_EDITOR
[CustomEditor(typeof(RampTexCreator))]
public class RampTexCreatorEditor : Editor
{
private RampTexCreator dst;
private void OnEnable()
{
dst = (RampTexCreator)target;
try
{
dst.dstMat = dst.GetComponent<Renderer>().sharedMaterial;
}
catch { }
}
private const string _RGBA = "RGBA";
private bool setMatFlag = false, grayMode = false;
public override void OnInspectorGUI()
{
//撤销堆栈
Undo.RecordObject(dst, "RampTexCreaterEditor");
//材质测试
GUILayout.Label("【材质测试】");
dst.dstMat = EditorGUILayout.ObjectField("目标材质", dst.dstMat, typeof(Material), true) as Material;
if (dst.dstMat)
{
dst.textureIdx = EditorGUILayout.Popup("纹理变量名", dst.textureIdx, dst.dstMat.GetTexturePropertyNames());//选择shader中的渐变纹理
setMatFlag = GUILayout.Toggle(setMatFlag, "传递纹理到材质");
if (setMatFlag)
{
dst.SetTexture();
}
}
//模式切换
GUILayout.Space(20);
GUILayout.Label("【参数控制】");
if (GUILayout.Button(dst.curveMode ? "曲线模式" : "梯度模式"))
{
dst.curveMode = dst.curveMode ? false : true;
}
dst.isLinear = GUILayout.Toggle(dst.isLinear, "线性空间颜色");
//曲线模式
if (dst.curveMode)
{
grayMode = GUILayout.Toggle(grayMode, "灰度模式");
for (int i = 0; i < (grayMode ? 1 : dst.curveRGBA.Length); i++)
{
if (dst.curveRGBA[i] == null)
{
dst.curveRGBA[i] = new AnimationCurve(new Keyframe(0, 0.5f), new Keyframe(1, 0.5f));
}
dst.curveRGBA[i] = EditorGUILayout.CurveField(_RGBA[i].ToString(), dst.curveRGBA[i]);
}
if (grayMode)
{
dst.curveRGBA[1] = new AnimationCurve(dst.curveRGBA[0].keys);
dst.curveRGBA[2] = new AnimationCurve(dst.curveRGBA[0].keys);
dst.curveRGBA[3] = new AnimationCurve(new Keyframe(0, 1), new Keyframe(1, 1));
}
//预览图
GUILayout.Label("", new GUIStyle { normal = new GUIStyleState { background = RampTexCreator.CreateRampTex(dst.isLinear, dst.curveRGBA) } });
}
//梯度模式
else
{
dst.gradient = EditorGUILayout.GradientField(dst.gradient);
}
//导出文件
if (GUILayout.Button("导出图像"))
{
Texture2D outTex = dst.curveMode ? RampTexCreator.CreateRampTex(dst.isLinear, dst.curveRGBA) : RampTexCreator.CreateRampTex(dst.isLinear, dst.gradient);
string texPath = "Assets/tex_NewRampTex.tga";//在Assets目录下生成图像文件
byte[] bytes = outTex.EncodeToTGA();
try
{
File.WriteAllBytes(texPath, bytes);
AssetDatabase.Refresh();
//修改纹理导入设置
TextureImporter textureImporter = AssetImporter.GetAtPath(texPath) as TextureImporter;
textureImporter.sRGBTexture = !dst.isLinear;
textureImporter.wrapMode = TextureWrapMode.Clamp;
//修改纹理平台设置
TextureImporterPlatformSettings textureImporterPlatformSettings = new TextureImporterPlatformSettings();
textureImporterPlatformSettings.maxTextureSize = 256;
textureImporter.SetPlatformTextureSettings(textureImporterPlatformSettings);
//强制刷新设置更改
AssetDatabase.WriteImportSettingsIfDirty(texPath);
AssetDatabase.ImportAsset(texPath, ImportAssetOptions.ForceUpdate);
//重定向
outTex = AssetDatabase.LoadAssetAtPath<Texture2D>(texPath);
Selection.activeObject = outTex;
dst.dstMat.SetTexture(dst.dstMat.GetTexturePropertyNames()[dst.textureIdx], outTex);
setMatFlag = false;
Debug.Log("导出成功!");
}
catch
{
Debug.Log("导出失败!");
}
}
}
}
#endif 需要说明的一点是关于那个【线性空间颜色】的设置。(关于颜色的线性空间和伽马空间矫正等概念具体可以百度- -简单来说就是因为人眼对暗色变化的敏感度高于亮色,所以伽马空间在现实物理的线性空间颜色基础上有一个大约2.2次方的指数运算,以使计算机存储更多的暗部细节)
首先可以先确认一下U3D项目的色彩空间,方式是上方Edit->Project Settings->Player->Other Settings->Rendering->Color Space。
如果用的是Gamma,那工具中线性空间颜色那个勾可以无视,效果是一样的;
如果是Linear,判断勾不勾这个选项的基本思路是看这个渐变纹理的用途是做颜色计算还是某种控制遮罩。如果是像案例中做光照计算则不需要勾;如果色值代表某种像是向量,高度或者强度之类的属性则需要勾上。(U3D图像导入设置里的sRGB勾不勾也是这个思路,只不过是相反的,程序中也是这样实现的)