我想分享一个酷酷的程序化NPC系统,我也想解释一下它是如何工作的(注意,这不是一个逐步指南,你需要有一定的Unity、Blender和Shader Graph经验才能复制这个系统)。
这个系统中,每个NPC都使用两个模型,一个男的模型和一个女的模型。通过使用Blend Shape来混合瘦和胖的版本的网格,来实现不同的身体类型。
颜色是通过一个自定义的着色器来实现的,这个着色器的工作原理如下:
- 在两个模型上都绘制了顶点颜色,皮肤区域是黑色,衬衫区域是红色,裤子区域是绿色,鞋子区域是蓝色。
- 模型的UV是通过Blender的Project from View unwrapping方法来实现的,腿部区域是通过这个方法来实现的,手臂区域是通过在手腕上标记一个缝合线,然后在手臂中间标记一个缝合线,直到手臂尽头,然后沿着整个身体中线标记一个缝合线,直到腰部。
- 着色器首先使用顶点颜色来确定皮肤区域,然后将衬衫颜色、裤子颜色和鞋子颜色叠加在上面。
- 然后为了允许像短裤和背心这样的物品,我们使用模型的UV作为参考点来确定顶点在模型中的位置(我们不使用模型的位置,因为这会在模型变形时打破)。然后我们使用一些基本的数学来说,如果任何像素低于这个特定阈值,我们就应用皮肤颜色。
为了生成具有多样化物品设置的NPC,同时使用同一个材质,我们使用了一个自定义脚本来随机化大量的材质设置。这个脚本将在下面的帖子中提供。
整个NPC着色器(注意:这个着色器有一些bug,但它工作得很好。如果你有任何改进的建议,我会非常感激。)
任何话都得到了,谢谢,因为这是一个非常酷的东西,我想记录一下。有人可能会问为什么需要这样做?不能简单地创建多个不同的模型来实现不同的服装?不能在运行时简单地交换纹理?不能简单地使用不同的材质?
答案是所有的都来到了性能。如果你需要创建大量不同版本的NPC(潜在上百个),那么所有这些模型都会占用大量的内存,这可能会影响其他东西,如更多的建筑或房屋。这个方法只需要两个模型就可以实现NPC的所有版本,这大大减少了内存的占用。交换纹理在运行时也是不理想的,因为你需要大量不同的纹理来实现相同的视觉多样性,这会导致游戏文件大小的膨胀。使用不同的材质也是不理想的,因为这会导致批处理问题,因为你无法将场景中具有不同材质的对象批处理在一起。这个方法解决了所有这些问题。由于颜色是通过程序生成的,你不需要担心纹理会导致文件大小膨胀;由于只需要两个模型就可以实现NPC的所有版本,你不需要担心模型会占用内存;由于NPC只使用一个材质,你不需要担心绘制调用。
这个方法的缺点是这个方法只适用于低多边形风格的游戏,而且实现起来也有一点困难。它不是很难,但需要一些耐心和耐心。另外,由于顶点颜色绘制,只有4个通道可用,所以你不能很灵活地处理服装。
TL-DR:
我创建了一个系统来程序化NPC。这个系统使用顶点颜色来确定皮肤区域,衬衫区域,裤子区域和鞋子区域。然后它使用模型的UV来确定顶点在模型中的位置,然后进行一个基本的检查,如果像素低于某个阈值,就应用皮肤颜色。这是为了实现像短裤和背心这样的物品。这个系统是为了节省内存,绘制调用和存储空间而设计的,因为NPC只使用两个模型和一个材质。
脚本用于生成NPC(脚本附在场景中的一个游戏对象上。脚本中属性的名称反映了我的着色器中属性的名称。如果你在你的着色器中使用相同的属性名称,你就可以直接复制这个脚本)。
using UnityEngine;
public class NPCSpawner : MonoBehaviour
{
[Header("References")]
public GameObject malePrefab;
public GameObject femalePrefab;
[Header("Spawn Settings")]
public float spawnRadius;
public int spawnAmount;
[Header("Variation Settings")]
public Vector2 bodyTypeRange;
public Vector2 shirtCutoffRange;
public Vector2 pantsCutoffRange;
public Vector2 sizeRange;
public Color[] raceColors;
public ColorProfile npcClothingColorProfile;
private static readonly int RaceColorPropID = Shader.PropertyToID("_Race_Color");
private static readonly int ShirtColorPropID = Shader.PropertyToID("_Shirt_Color");
private static readonly int PantsColorPropID = Shader.PropertyToID("_Pants_Color");
private static readonly int ShoesColorPropID = Shader.PropertyToID("_Shoes_Color");
private static readonly int ShirtCutoffPropID = Shader.PropertyToID("_Shirt_Cutoff");
private static readonly int PantsCutoffPropID = Shader.PropertyToID("_Pants_Cutoff");
public enum ColorProfile
{
Unsaturated,
HighlySaturated,
Dark,
Navy
}
void Start()
{
SpawnNPCS();
}
void SpawnNPCS()
{
for (int i = 0; i < spawnAmount / 2; i++)
{
SpawnNPC(malePrefab);
}
for (int i = 0; i < spawnAmount / 2; i++)
{
SpawnNPC(femalePrefab);
}
}
void OnDrawGizmosSelected()
{
Gizmos.DrawWireSphere(transform.position, spawnRadius);
Gizmos.color = Color.blue;
}
void SpawnNPC(GameObject npcToSpawn)
{
GameObject npcPrefabDupe = RandomSpawnPoint(npcToSpawn);
SkinnedMeshRenderer npcSkinnedMesh = npcPrefabDupe.GetComponentInChildren<SkinnedMeshRenderer>();
SetRandomBodyType(npcSkinnedMesh, bodyTypeRange);
RandomColorWithArray(npcSkinnedMesh, raceColors, RaceColorPropID);
RandomColorWithProfile(npcSkinnedMesh, npcClothingColorProfile, ShirtColorPropID);
RandomColorWithProfile(npcSkinnedMesh, npcClothingColorProfile, PantsColorPropID);
RandomColorWithProfile(npcSkinnedMesh, npcClothingColorProfile, ShoesColorPropID);
SetRandomCutoff(npcSkinnedMesh, shirtCutoffRange, ShirtCutoffPropID);
SetRandomCutoff(npcSkinnedMesh, pantsCutoffRange, PantsCutoffPropID);
SetRandomSize(npcPrefabDupe, sizeRange);
}
GameObject RandomSpawnPoint(GameObject prefab)
{
Vector2 randomPoint = Random.insideUnitCircle * spawnRadius;
Vector3 spawnPos = transform.position + new Vector3(randomPoint.x, 0, randomPoint.y);
GameObject prefabDupe = Instantiate(prefab, spawnPos, Quaternion.identity);
return prefabDupe;
}
void SetRandomBodyType(SkinnedMeshRenderer skinnedMesh, Vector2 range)
{
skinnedMesh.SetBlendShapeWeight(0, Random.Range(range.x, range.y));
}
void SetRandomSize(GameObject prefabDupe, Vector2 range)
{
Vector3 targetScale = new Vector3(1f, Random.Range(range.x, range.y), 1f);
prefabDupe.transform.localScale = targetScale;
}
void SetRandomCutoff(SkinnedMeshRenderer skinnedMesh, Vector2 range, int cutoffID)
{
float targetCutoff = Random.Range(range.x, range.y);
skinnedMesh.material.SetFloat(cutoffID, targetCutoff);
}
void RandomColorWithArray(SkinnedMeshRenderer skinnedMesh, Color[] colors, int propID)
{
int randomIndex = Random.Range(0, colors.Length);
Color chosenColor = colors[randomIndex];
skinnedMesh.material.SetColor(propID, chosenColor);
}
void RandomColorWithProfile(SkinnedMeshRenderer skinnedMesh, ColorProfile colorProfile, int propID)
{
float minH = 0f, maxH = 1f;
float minS = 0f, maxS = 1f;
float minV = 0f, maxV = 1f;
switch (colorProfile)
{
case ColorProfile.Unsaturated:
minS = 0.1f; maxS = 0.35f;
minV = 0.75f; maxV = 0.95f;
break;
case ColorProfile.HighlySaturated:
minS = 0.85f; maxS = 1.0f;
minV = 0.80f; maxV = 1.0f;
break;
case ColorProfile.Dark:
minS = 0.3f; maxS = 0.9f;
minV = 0.15f; maxV = 0.35f;
break;
case ColorProfile.Navy:
// Restrict hue mapping strictly to the blue spectrum
minH = 0.58f; maxH = 0.66f;
minS = 0.65f; maxS = 0.95f;
minV = 0.15f; maxV = 0.45f;
break;
}
float randomH = Random.Range(minH, maxH);
float randomS = Random.Range(minS, maxS);
float randomV = Random.Range(minV, maxV);
Color chosenColor = Color.HSVToRGB(randomH, randomS, randomV);
skinnedMesh.material.SetColor(propID, chosenColor);
}
}
评论 (0)