在上一篇文章(建筑场景的阴影实现一)中我们实现了想要的效果,这篇文章我们来优化一下在项目中的实际运用,以及编写一些脚本工具提升制作效率。

首先是一个在Maya里面批量给模型生成第二套UV,并自动映射的小脚本。

string $objects[] = `ls -sl`;
string $uvName = "uv2";
for($obj in $objects){
polyAutoProjection -createNewMap 1 -uvSetName $uvName -layoutMethod 0 -projectBothDirections 0 -insertBeforeDeformers 1
-layout 2 -scaleMode 1 -optimize 0 -percentageSpace 0.2 -worldSpace 0 $obj;
}

这是个Maya的Mel脚本,复制到ScriptEditor中,然后在场景中选择所有的建筑模型执行即可。

接下来是批量烘焙阴影贴图,先将模型在场景中摆好并打好灯光,模型之间要摆开不能有穿插。

然后打开RenderSetting面板,设置下输出烘焙贴图的路径名字和贴图格式。

File name prefix这里一定要添加<Object>,这样每个模型输出的烘焙贴图就会加上模型的名字,要不然每执行一次烘焙,输出的贴图都会替换掉之前的。

在打开RedShift的BakingOption面板

设置好贴图大小之后选择场景中所有需要烘焙的模型然后点击Bake执行批量烘焙

烘焙完成之后就可以看到所有的模型阴影贴图都输出到了指定的路径了。

接下来我们在做一些优化,我们的建筑Color贴图都是共用一张2048的贴图,这样也就意味著我们场景中的建筑只需要一张贴图,一个Material.现在我们要把这么多建筑烘焙出来的阴影贴图都放在这张贴图的Alpha通道里面。在这里我们需要合并烘焙出来的阴影贴图,然后调整建筑第二套UV的位置。我们需要根据建筑实际的数量来调整大小和位置,如果我们烘焙的贴图是1024的,那么一张2048的贴图只能存储4个显然是不够的,所以我们要调整烘焙贴图的尺寸。一张2048的贴图可以存储4张1024,16张512,64张256, 256张128的,存储的数量越多单个的阴影解析度也就越低,不过好在阴影的采样对精度的要求不是那么高,在128都能够接受。这里我们测试9个模型,就用512来举例。

我们需要合并贴图的操作就是上面图中的效果。看到上面PS里面的图片之后是不是觉得很方......如果有上百个建筑那是不是要手动去PS里面摆上百张贴图???哈哈哈,放心不用紧张,作为一个有责任感的TA是绝对不可能允许这种级没有技术含量又低效率的操作出现。现在就给大家献上在Unity中写的一个自动合并贴图的脚本工具,这是个编辑器脚本,所以要放在Editor目录下使用。

using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;

public class CombineTextures : EditorWindow
{
private static int textureSize = 2048;
private static int scaleSize = 512;
private static string saveTexName = "CombineTex.png";
private static string texSavePath = "Assets/ArtRes/Building/Texture/";

//脚本命令在工具栏的位置
[MenuItem("Tools/CombineTextures")]

//创建一个窗口用来设置参数
static void createWindow()
{
CombineTextures initWindow = GetWindow<CombineTextures>();
initWindow.titleContent = new GUIContent("批量合并贴图");
initWindow.maxSize = new Vector2(470 , 180);
initWindow.minSize = new Vector2(470 , 180);
}

//窗口面板的UI绘制,我们需要设置的参数介面
private void OnGUI()
{
GUILayout.BeginVertical();
GUILayout.Space(10);
GUI.skin.label.fontSize = 14;
GUI.skin.label.alignment = TextAnchor.MiddleLeft;
int selCount = Selection.objects.Length;
string countToStr = selCount.ToString();

//贴图名称
GUILayout.Space(10);
GUILayout.BeginHorizontal();
GUILayout.Label("贴图名称:" , GUILayout.Width(100));
saveTexName = GUILayout.TextField(saveTexName , GUILayout.Width(150) , GUILayout.Height(18));
GUILayout.Space(30);
GUILayout.Label("贴图数量:" , GUILayout.Width(100));
GUILayout.Label(countToStr , GUILayout.Width(100));
GUILayout.EndHorizontal();

//保存的路径
GUILayout.Space(10);
GUILayout.BeginHorizontal();
GUILayout.Label("保存路径:" , GUILayout.Width(100));
texSavePath = EditorGUILayout.TextField( texSavePath , GUILayout.Width(280) , GUILayout.Height(18));
if (GUILayout.Button("浏览" , GUILayout.Width(65) , GUILayout.Height(18)))
{
texSavePath = EditorUtility.OpenFolderPanel("Save Path" , texSavePath , "");
texSavePath += "/";
}
GUILayout.EndHorizontal();

//设置贴图大小
GUILayout.Space(10);
GUILayout.BeginHorizontal();
GUILayout.Label("图集大小:" , GUILayout.Width(100));
textureSize = EditorGUILayout.IntField(textureSize , GUILayout.Width(100) , GUILayout.Height(18));
GUILayout.Space(40);
GUILayout.Label("贴图大小:" , GUILayout.Width(100));
scaleSize = EditorGUILayout.IntField(scaleSize , GUILayout.Width(100) , GUILayout.Height(18));
GUILayout.EndHorizontal();

//生成或者取消
GUILayout.Space(30);
GUILayout.BeginHorizontal();
GUILayout.Space(100);
if (GUILayout.Button("生成" , GUILayout.Width(85) , GUILayout.Height(25)))
{
if (selCount == 0)
{
EditorUtility.DisplayDialog("Error" , "请选择需要合并的贴图!" , "Ok");
return;
}
else if (texSavePath == null)
{
EditorUtility.DisplayDialog("Error" , "请选择贴图存放的路径!" , "Ok");
return;
}
else
{
CAG_CombineTextures();
}
}
GUILayout.Space(100);
if (GUILayout.Button("取消" , GUILayout.Width(85) , GUILayout.Height(25)))
{
this.Close();
}
GUILayout.EndHorizontal();
}

static void CAG_CombineTextures()
{

Object[] selObjs = Selection.objects;
Texture2D[] texArray = new Texture2D[selObjs.Length];
Texture2D atlaseTex = new Texture2D(textureSize , textureSize);
int columnCount = textureSize / scaleSize;
int row = 0;
int column = 0;

for(int i = 0; i < selObjs.Length; i++)
{
texArray[i] = selObjs[i] as Texture2D;
string assetPath = AssetDatabase.GetAssetPath(selObjs[i]);
setTexReadable(assetPath);
row = i / columnCount;
column = i % columnCount;
//Debug.Log(row + " : " + column);
SetAtlasColor(atlaseTex , texArray[i] , scaleSize , row , column);
}

atlaseTex.Apply();
var bytes = atlaseTex.EncodeToPNG();

File.WriteAllBytes(texSavePath + saveTexName , bytes);
//AssetDatabase.ImportAsset(texSavePath + saveTexName);
AssetDatabase.Refresh();
}

//将缩放后的贴图储存在合并的图集里面
static void SetAtlasColor(Texture2D atlaseTex, Texture2D selectTex, int scaleSize, int row, int column)
{
Texture2D scaleTex = ScaleTexture(selectTex , scaleSize , scaleSize);
Color[] colors = scaleTex.GetPixels();
atlaseTex.SetPixels(column * scaleTex.width , textureSize - scaleTex.height * (row+1) , scaleTex.width , scaleTex.height , colors);
}

//对贴图做缩放处理
static Texture2D ScaleTexture(Texture2D source , int targetWidth , int targetHeight)
{
Texture2D result = new Texture2D(targetWidth , targetHeight , TextureFormat.RGBA32 , false);

for (int i = 0; i < result.height; i++)
{
for (int j = 0; j < result.width; j++)
{
Color newColor = source.GetPixelBilinear((float)j / (float)result.width , (float)i / (float)result.height);
result.SetPixel(j , i , newColor);
}
}
result.Apply();
return result;
}

//首先要将需要合并的贴图的Readable属性打开,否则是不能获取到贴图的像素信息
static void setTexReadable(string texPathName)
{
TextureImporter ti = (TextureImporter)TextureImporter.GetAtPath(texPathName);
ti.isReadable = true;
AssetDatabase.ImportAsset(texPathName);
}
}

上面这个脚本主要的内容就是将每张阴影贴图先做一次缩放处理,缩放到我们在合并后图集中的大小(这里我们举例是512的大小),然后在将缩放后的像素信息按照行列的索引储存到合并的图集里面。上面的代码中编辑器的UI,还有行列的索引for循环,这些就不详细讲了。还有不理解的同学可以去找下资料,主要是Texture2D.GetPixels,Texture2D.SetPixels的使用。

在工具栏点击了CombineTextures命令之后就会弹出一个编辑器的窗口了,接下来就是要设置下贴图的名称,保存的路径,图集的大小以及每张贴图缩放后的大小。如下图:

然后在资源管理器中去选中我们需要合并的贴图,点击生成按钮,等脚本执行完成之后就会在设置的路径中生成一张新的合并之后的贴图了。

新生成的这张贴图也就是上面在PS里面看到的那张了。怎么样不用人工手动在PS里面摆是不是很开心!

合并完了贴图之后,我们又要回到Maya里面来处理每个建筑第二套UV的位置了。原理跟合并贴图是类似的,只是UV没有尺寸的概念,他是xy轴向上0-1的范围。上面我们合并贴图的时候设置了合并后每张贴图的大小是512,那么原来的UV的大小就要缩放成512/2048=0.25,然后在按照行列的索引去做位移。当然代码也放上来了,还是Maya的Mel脚本,由于时间关系以及MayaMel写UI窗口比较蛋疼,这个脚本就没有写界面了,直接在脚本编辑器里面改参数执行吧。。。

//1.设置图集的大小
//2.设置合并后每张贴图的大小
//3.设置需要修改调整的UV名称
//4.开始调整的UV索引
//脚本是可以做批量处理的,如果有需要单独修改某一个建筑或者某个UV通道也是可以的
//-------------------------------------------------------------------------------------------------------------------
//设置图集的大小,有几套UV需要调整就设置几个大小,这三个参数($atlaseSize,$targetSize,$UVSetsNameList)的值是对应的
//$atlaseSize[1],$targetSize[1],$UVSetsNameList[1],这样就是对模型的uv2做调整,调整后是512的贴图在2048的贴图中所占用的位置
float $atlaseSize[] = {2048, 2048, 2048};
float $targetSize[] = {512, 512, 512};
//设置需要调整的UV列表,1.默认的Color采样的第一套UV,2.阴影采样的第二套UV,3.接收地面投影的面片UV
string $UVSetsNameList[] = {"UVChannel_1", "uv2", "map1"};
//从哪套UV的索引开始,如果要修改不需要调整uv2,只是调整map1的话就可以将值改成从1开始
int $startUVSet = 1;
//如果需要单独调整某一个模型的UV时在这里设置物体的名称,然后执行下面的DoEditUVs
string $objName = "";

//批量处理调整UV,需要选择场景中的建筑模型,会把模型下面的子物体接收投影的面片的UV也按照设置的参数调整,不需要重新选择执行
SetUVSets($startUVSet, $atlaseSize, $targetSize, $UVSetsNameList);
//对某个单独的模型和UV通道做调整
//DoEditUVs($objName, $UVSetsNameList[1], $targetSize[1] / $atlaseSize[2], 0, 0);
//还原某个单独的模型和UV通道
//ResetUVs($objName, $UVSetsNameList[1], $targetSize[$i] / $atlaseSize[$i], 0, 0);

global proc SetUVSets(int $startUVSet, float $atlaseSize[], float $targetSize[], string $UVSetsNameList[]){

string $selObjsList[] = `ls -sl`;

for($i=$startUVSet; $i<3; $i++){
float $scale = $targetSize[$i] / $atlaseSize[$i];
float $columnNum = floor(1 / $scale);
int $index = 0;
for($r = 0 ; $r < $columnNum; $r++){
for($c = 0; $c < $columnNum; $c++){
if($i < 2 && $index < size($selObjsList)){
DoEditUVs($selObjsList[$index], $UVSetsNameList[$i], $scale, $r, $c);
}
if($i == 2 && $index < size($selObjsList)){
string $objsList[] = `listRelatives -c $selObjsList[$index]`;
DoEditUVs($objsList[1], $UVSetsNameList[$i], $scale, $r, $c);
}
$index++;
}
}
}
}

global proc DoEditUVs(string $selObj, string $UVSet, float $scale, float $row, float $column){
polyUVSet -currentUVSet -uvSet $UVSet $selObj;
int $uvsCount[] = `polyEvaluate -uvcoord -uvSetName $UVSet $selObj`;
string $editUVName = $selObj + ".map[0:" + ($uvsCount[0] - 1) + "]";
float $move = $scale/2 - 0.5;
float $rowPos = $row * $scale * -1;
float $columnPos = $column * $scale;
polyEditUV -pivotU 0.5 -pivotV 0.5 -scaleU $scale -scaleV $scale $editUVName;
polyEditUV -relative true -uValue $move -vValue `abs($move)` $editUVName;
polyEditUV -relative true -uValue $columnPos -vValue $rowPos $editUVName;
}

global proc ResetUVs(string $selObj, string $UVSet, float $scale, float $row, float $column){
polyUVSet -currentUVSet -uvSet $UVSet $selObj;
int $uvsCount[] = `polyEvaluate -uvcoord -uvSetName $UVSet $selObj`;
string $editUVName = $selObj + ".map[0:" + ($uvsCount[0] - 1) + "]";
float $move = $scale / 2 - 0.5;
float $rowPos = $row * $scale;
float $columnPos = $column * $scale * -1;
polyEditUV -relative true -uValue $columnPos -vValue $rowPos $editUVName;
polyEditUV -relative true -uValue `abs($move)` -vValue $move $editUVName;
polyEditUV -pivotU 0.5 -pivotV 0.5 -scaleU (1/$scale) -scaleV (1/$scale) $editUVName;
}

这里注意一下,我们项目中建筑的贴图在制作的时候就是去共用一张2048的贴图,所以不需要去调整采样Color的第一套UV,$startUVSet的值就设置成1,跳过第一套UV的设置,如果有同学是要连Color贴图也合并的话可以把这个值设成0。下面就是调整过后的UV了,这样就UV就更合并后的贴图能够匹配上了。

这样我们在Unity里面所有的建筑就可以用一个Material和一张贴图搞定啦

下面一篇文章我们在游戏运行状态下来看看性能的开销情况。


推荐阅读:
相关文章