在 Unity 中使用自定义着色器正确平铺图集纹理
Tiling atlas textures correctly with a custom shader in Unity
这有点复杂,但我希望归结起来是一个非常简单的问题。所以事情是这样的:我使用 Unity 在运行时从一个 bsp 文件生成一个地图游戏对象,该文件具有一大堆顶点、面、uvs、纹理引用等。创建的网格完全按照它们应该的样子出现,所有纹理都很好。但是有一个问题,用如此多的材料创建了如此多的网格,导致许多绘制调用,从而使程序变慢。所以我搜索了一种减少绘制调用的方法,并找到了解决方案。将所有网格组合成一个大网格,并通过组合所有使用的纹理来创建纹理图集。结合网格效果很好,结合纹理效果也很好。然后我遇到了uv贴图的问题。所以我从 NVidia 白皮书中找到了一个解决方案来制作一个自定义着色器,它使用 tex2d 函数使用 uv 位置及其导数从纹理中插入纹素。我认为这会奏效,但我的网格有非常奇怪的三角形,我认为它们正在破坏这个解决方案。在下图中,您可以看到网格合并时与分开时的区别:
Combined Meshes with Changed UVs and Custom Shader
Separate Meshes with original UVs
这是我在着色器中用来设置模型颜色的代码:
o.Albedo = tex2D (_MainTex, IN.uv2_BlendTex, ddx(IN.uv_MainTex), ddy(IN.uv_MainTex)).rgb;
如您所见,我添加了第二个 UV,它是原始 UV 的非平铺版本。我通过使用 frac() 函数来做到这一点,但在 C# 代码中而不是在着色器中。由于纹理可以有不同的大小,我必须在进入着色器之前计算 UV,因为那时我可以访问纹理大小。
这是我用来计算 2 个 UV 的代码:
Rect surfaceTextureRect = uvReMappers[textureIndex];
Mesh surfaceMesh = allFaces[i].mesh;
Vector2[] atlasTiledUVs = new Vector2[surfaceMesh.uv.Length];
Vector2[] atlasClampedUVs = new Vector2[surfaceMesh.uv.Length];
for (int j = 0; j < atlasClampedUVs.Length; j++)
{
Vector2 clampedUV = new Vector2((surfaceMesh.uv[j].x - Mathf.Floor(surfaceMesh.uv[j].x)), (surfaceMesh.uv[j].y - Mathf.Floor(surfaceMesh.uv[j].y)));
float atlasClampedX = (clampedUV.x * surfaceTextureRect.width) + surfaceTextureRect.x;
float atlasClampedY = (clampedUV.y * surfaceTextureRect.height) + surfaceTextureRect.y;
atlasTiledUVs[j] = new Vector2((surfaceMesh.uv[j].x * surfaceTextureRect.width) + surfaceTextureRect.x, (surfaceMesh.uv[j].y * surfaceTextureRect.height) + surfaceTextureRect.y);
atlasClampedUVs[j] = new Vector2(atlasClampedX, atlasClampedY);
if (i < 10) { Debug.Log(i + " Original: " + surfaceMesh.uv[j] + " ClampedUV: " + clampedUV); }
}
surfaceMesh.uv = atlasTiledUVs;
surfaceMesh.uv2 = atlasClampedUVs;
数组 uvReMappers 是使用 Texture2D 函数 PackTextures() 时创建的 Rect 数组。
抱歉花了这么长时间,但这是我的问题:为什么纹理会扭曲。是因为网格的三角化方式还是因为我编写自定义着色器的方式。最后我该如何解决它。
感谢您的宝贵时间。很抱歉写了这么多,但我以前从未发过问题。我总是在网上找到几乎所有问题的答案,但我已经搜索了好几天如何解决这个问题。我觉得它可能太具体而无法找到答案。希望我提供了足够的信息。
我采用了不同的方法并在 cpu 上创建了一个纹理图集,从那里 UV 贴图就像普通的 UV 贴图一样,我所要做的就是将纹理分配给图集中的顶点信息。 ..
我的场景是一个自定义体素引擎,它可以处理从我的世界到渲染基于体素的行星的任何事情,我还没有找到它无法处理的场景。
这是我的地图集代码...
using UnityEngine;
using Voxels.Objects;
namespace Engine.MeshGeneration.Texturing
{
/// <summary>
/// Packed texture set to be used for mapping texture info on
/// dynamically generated meshes.
/// </summary>
public class TextureAtlas
{
/// <summary>
/// Texture definitions within the atlas.
/// </summary>
public TextureDef[] Textures { get; set; }
public TextureAtlas()
{
SetupTextures();
}
protected virtual void SetupTextures()
{
// default for bas atlas is a material with a single texture in the atlas
Textures = new TextureDef[]
{
new TextureDef
{
VoxelType = 0,
Faces = new[] { Face.Top, Face.Bottom, Face.Left, Face.Right, Face.Front, Face.Back },
Bounds = new[] {
new Vector2(0,1),
new Vector2(1, 1),
new Vector2(1,0),
new Vector2(0, 0)
}
}
};
}
public static TextureDef[] GenerateTextureSet(IntVector2 textureSizeInPixels, IntVector2 atlasSizeInPixels)
{
int x = atlasSizeInPixels.X / textureSizeInPixels.X;
int z = atlasSizeInPixels.Z / textureSizeInPixels.Z;
int i = 0;
var result = new TextureDef[x * z];
var uvSize = new Vector2(1f / ((float)x), 1f / ((float)z));
for (int tx = 0; tx < x; tx++)
for (int tz = 0; tz < z; tz++)
{
// for perf, types are limited to 255 (1 byte)
if(i < 255)
{
result[i] = new TextureDef
{
VoxelType = (byte)i,
Faces = new[] { Face.Top, Face.Bottom, Face.Left, Face.Right, Face.Front, Face.Back },
Bounds = new[] {
new Vector2(tx * uvSize.x, (tz + 1f) * uvSize.y),
new Vector2((tx + 1f) * uvSize.x, (tz + 1f) * uvSize.y),
new Vector2((tx + 1f) * uvSize.x, tz * uvSize.y),
new Vector2(tx * uvSize.x, tz * uvSize.y)
}
};
i++;
}
else
break;
}
return result;
}
}
}
并且对于图集中的纹理定义...
using UnityEngine;
using Voxels.Objects;
namespace Engine.MeshGeneration.Texturing
{
/// <summary>
/// Represents an area within the atlas texture
/// from which a single texture can be pulled.
/// </summary>
public class TextureDef
{
/// <summary>
/// The voxel block type to use this texture for.
/// </summary>
public byte VoxelType { get; set; }
/// <summary>
/// Faces this texture should be applied to on voxels of the above type.
/// </summary>
public Face[] Faces { get; set; }
/// <summary>
/// Atlas start ref
/// </summary>
public Vector2[] Bounds { get; set; }
}
}
对于我需要直接控制 UV 映射的自定义场景,我继承了纹理图集,然后重写了 SetupTextures() 方法,但在几乎所有情况下,对我来说,我创建的纹理都是相同大小的图集非常简单调用 GenerateTextureSet 将进行 uv 映射计算,我相信你需要。
给定体素类型的给定面的 UV 坐标为...
IEnumerable<Vector2> UVCoords(byte voxelType, Face face, TextureAtlas atlas)
{
return atlas.Textures
.Where(a => a.VoxelType == voxelType && a.Faces.Contains(face))
.First()
.Bounds;
}
在你的情况下,你可能有不同的方式来映射到你的包中选择的纹理,但在我的情况下,基本上是面部和类型的组合决定了我想要的 uv 映射集。
这样您就可以将网格与任何标准着色器一起使用,而不是依赖自定义着色器逻辑。
我终于解决了问题!所以事实证明我不应该在着色器之前计算 UV。相反,我通过 UV 传递着色器所需的信息,以便它可以直接计算新的纹素位置。
这是着色器之前的代码:
Rect surfaceTextureRect = uvReMappers[textureIndex];
Mesh surfaceMesh = allFaces[i].mesh;
Vector2[] atlasTexturePosition = new Vector2[surfaceMesh.uv.Length];
Vector2[] atlasTextureSize = new Vector2[surfaceMesh.uv.Length];
for (int j = 0; j < atlasTexturePosition.Length; j++)
{
atlasTexturePosition[j] = new Vector2(surfaceTextureRect.x, surfaceTextureRect.y);
atlasTextureSize[j] = new Vector2(surfaceTextureRect.width, surfaceTextureRect.height);
}
surfaceMesh.uv2 = atlasTexturePosition;
surfaceMesh.uv3 = atlasTextureSize;
这是着色器代码:
tex2D(_MainTex, float2((frac(IN.uv.x) * IN.uv3.x) + IN.uv2.x, (frac(IN.uv.y) * IN.uv3.y) + IN.uv2.y));
你得把传入的TEXCOORD0
从图像的百分比space变成一个像素值,用模数算出它在平铺纹理上的哪个像素,然后将其恢复为图像的百分比。
代码如下:
您需要定义 2D
变量 _MainTex
和 _PatternTex
。
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
float modFunction(float number, float divisor){
//2018-05-24: copied from an answer by Nicol Bolas:
return (number - (divisor * floor(number/divisor)));
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 curColor = tex2D(_MainTex, i.uv);
fixed4 pattern = tex2D(_PatternTex,
float2(
modFunction(i.uv.x*_MainTex_TexelSize.z,_PatternTex_TexelSize.z) *_PatternTex_TexelSize.x,
modFunction(i.uv.y*_MainTex_TexelSize.w,_PatternTex_TexelSize.w) *_PatternTex_TexelSize.y
)
);
fixed4 col = curColor * pattern;
col.rgb *= col.a;
return col;
}
这有点复杂,但我希望归结起来是一个非常简单的问题。所以事情是这样的:我使用 Unity 在运行时从一个 bsp 文件生成一个地图游戏对象,该文件具有一大堆顶点、面、uvs、纹理引用等。创建的网格完全按照它们应该的样子出现,所有纹理都很好。但是有一个问题,用如此多的材料创建了如此多的网格,导致许多绘制调用,从而使程序变慢。所以我搜索了一种减少绘制调用的方法,并找到了解决方案。将所有网格组合成一个大网格,并通过组合所有使用的纹理来创建纹理图集。结合网格效果很好,结合纹理效果也很好。然后我遇到了uv贴图的问题。所以我从 NVidia 白皮书中找到了一个解决方案来制作一个自定义着色器,它使用 tex2d 函数使用 uv 位置及其导数从纹理中插入纹素。我认为这会奏效,但我的网格有非常奇怪的三角形,我认为它们正在破坏这个解决方案。在下图中,您可以看到网格合并时与分开时的区别:
Combined Meshes with Changed UVs and Custom Shader
Separate Meshes with original UVs
这是我在着色器中用来设置模型颜色的代码:
o.Albedo = tex2D (_MainTex, IN.uv2_BlendTex, ddx(IN.uv_MainTex), ddy(IN.uv_MainTex)).rgb;
如您所见,我添加了第二个 UV,它是原始 UV 的非平铺版本。我通过使用 frac() 函数来做到这一点,但在 C# 代码中而不是在着色器中。由于纹理可以有不同的大小,我必须在进入着色器之前计算 UV,因为那时我可以访问纹理大小。
这是我用来计算 2 个 UV 的代码:
Rect surfaceTextureRect = uvReMappers[textureIndex];
Mesh surfaceMesh = allFaces[i].mesh;
Vector2[] atlasTiledUVs = new Vector2[surfaceMesh.uv.Length];
Vector2[] atlasClampedUVs = new Vector2[surfaceMesh.uv.Length];
for (int j = 0; j < atlasClampedUVs.Length; j++)
{
Vector2 clampedUV = new Vector2((surfaceMesh.uv[j].x - Mathf.Floor(surfaceMesh.uv[j].x)), (surfaceMesh.uv[j].y - Mathf.Floor(surfaceMesh.uv[j].y)));
float atlasClampedX = (clampedUV.x * surfaceTextureRect.width) + surfaceTextureRect.x;
float atlasClampedY = (clampedUV.y * surfaceTextureRect.height) + surfaceTextureRect.y;
atlasTiledUVs[j] = new Vector2((surfaceMesh.uv[j].x * surfaceTextureRect.width) + surfaceTextureRect.x, (surfaceMesh.uv[j].y * surfaceTextureRect.height) + surfaceTextureRect.y);
atlasClampedUVs[j] = new Vector2(atlasClampedX, atlasClampedY);
if (i < 10) { Debug.Log(i + " Original: " + surfaceMesh.uv[j] + " ClampedUV: " + clampedUV); }
}
surfaceMesh.uv = atlasTiledUVs;
surfaceMesh.uv2 = atlasClampedUVs;
数组 uvReMappers 是使用 Texture2D 函数 PackTextures() 时创建的 Rect 数组。
抱歉花了这么长时间,但这是我的问题:为什么纹理会扭曲。是因为网格的三角化方式还是因为我编写自定义着色器的方式。最后我该如何解决它。
感谢您的宝贵时间。很抱歉写了这么多,但我以前从未发过问题。我总是在网上找到几乎所有问题的答案,但我已经搜索了好几天如何解决这个问题。我觉得它可能太具体而无法找到答案。希望我提供了足够的信息。
我采用了不同的方法并在 cpu 上创建了一个纹理图集,从那里 UV 贴图就像普通的 UV 贴图一样,我所要做的就是将纹理分配给图集中的顶点信息。 ..
我的场景是一个自定义体素引擎,它可以处理从我的世界到渲染基于体素的行星的任何事情,我还没有找到它无法处理的场景。
这是我的地图集代码...
using UnityEngine;
using Voxels.Objects;
namespace Engine.MeshGeneration.Texturing
{
/// <summary>
/// Packed texture set to be used for mapping texture info on
/// dynamically generated meshes.
/// </summary>
public class TextureAtlas
{
/// <summary>
/// Texture definitions within the atlas.
/// </summary>
public TextureDef[] Textures { get; set; }
public TextureAtlas()
{
SetupTextures();
}
protected virtual void SetupTextures()
{
// default for bas atlas is a material with a single texture in the atlas
Textures = new TextureDef[]
{
new TextureDef
{
VoxelType = 0,
Faces = new[] { Face.Top, Face.Bottom, Face.Left, Face.Right, Face.Front, Face.Back },
Bounds = new[] {
new Vector2(0,1),
new Vector2(1, 1),
new Vector2(1,0),
new Vector2(0, 0)
}
}
};
}
public static TextureDef[] GenerateTextureSet(IntVector2 textureSizeInPixels, IntVector2 atlasSizeInPixels)
{
int x = atlasSizeInPixels.X / textureSizeInPixels.X;
int z = atlasSizeInPixels.Z / textureSizeInPixels.Z;
int i = 0;
var result = new TextureDef[x * z];
var uvSize = new Vector2(1f / ((float)x), 1f / ((float)z));
for (int tx = 0; tx < x; tx++)
for (int tz = 0; tz < z; tz++)
{
// for perf, types are limited to 255 (1 byte)
if(i < 255)
{
result[i] = new TextureDef
{
VoxelType = (byte)i,
Faces = new[] { Face.Top, Face.Bottom, Face.Left, Face.Right, Face.Front, Face.Back },
Bounds = new[] {
new Vector2(tx * uvSize.x, (tz + 1f) * uvSize.y),
new Vector2((tx + 1f) * uvSize.x, (tz + 1f) * uvSize.y),
new Vector2((tx + 1f) * uvSize.x, tz * uvSize.y),
new Vector2(tx * uvSize.x, tz * uvSize.y)
}
};
i++;
}
else
break;
}
return result;
}
}
}
并且对于图集中的纹理定义...
using UnityEngine;
using Voxels.Objects;
namespace Engine.MeshGeneration.Texturing
{
/// <summary>
/// Represents an area within the atlas texture
/// from which a single texture can be pulled.
/// </summary>
public class TextureDef
{
/// <summary>
/// The voxel block type to use this texture for.
/// </summary>
public byte VoxelType { get; set; }
/// <summary>
/// Faces this texture should be applied to on voxels of the above type.
/// </summary>
public Face[] Faces { get; set; }
/// <summary>
/// Atlas start ref
/// </summary>
public Vector2[] Bounds { get; set; }
}
}
对于我需要直接控制 UV 映射的自定义场景,我继承了纹理图集,然后重写了 SetupTextures() 方法,但在几乎所有情况下,对我来说,我创建的纹理都是相同大小的图集非常简单调用 GenerateTextureSet 将进行 uv 映射计算,我相信你需要。
给定体素类型的给定面的 UV 坐标为...
IEnumerable<Vector2> UVCoords(byte voxelType, Face face, TextureAtlas atlas)
{
return atlas.Textures
.Where(a => a.VoxelType == voxelType && a.Faces.Contains(face))
.First()
.Bounds;
}
在你的情况下,你可能有不同的方式来映射到你的包中选择的纹理,但在我的情况下,基本上是面部和类型的组合决定了我想要的 uv 映射集。
这样您就可以将网格与任何标准着色器一起使用,而不是依赖自定义着色器逻辑。
我终于解决了问题!所以事实证明我不应该在着色器之前计算 UV。相反,我通过 UV 传递着色器所需的信息,以便它可以直接计算新的纹素位置。
这是着色器之前的代码:
Rect surfaceTextureRect = uvReMappers[textureIndex];
Mesh surfaceMesh = allFaces[i].mesh;
Vector2[] atlasTexturePosition = new Vector2[surfaceMesh.uv.Length];
Vector2[] atlasTextureSize = new Vector2[surfaceMesh.uv.Length];
for (int j = 0; j < atlasTexturePosition.Length; j++)
{
atlasTexturePosition[j] = new Vector2(surfaceTextureRect.x, surfaceTextureRect.y);
atlasTextureSize[j] = new Vector2(surfaceTextureRect.width, surfaceTextureRect.height);
}
surfaceMesh.uv2 = atlasTexturePosition;
surfaceMesh.uv3 = atlasTextureSize;
这是着色器代码:
tex2D(_MainTex, float2((frac(IN.uv.x) * IN.uv3.x) + IN.uv2.x, (frac(IN.uv.y) * IN.uv3.y) + IN.uv2.y));
你得把传入的TEXCOORD0
从图像的百分比space变成一个像素值,用模数算出它在平铺纹理上的哪个像素,然后将其恢复为图像的百分比。
代码如下:
您需要定义 2D
变量 _MainTex
和 _PatternTex
。
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
float modFunction(float number, float divisor){
//2018-05-24: copied from an answer by Nicol Bolas:
return (number - (divisor * floor(number/divisor)));
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 curColor = tex2D(_MainTex, i.uv);
fixed4 pattern = tex2D(_PatternTex,
float2(
modFunction(i.uv.x*_MainTex_TexelSize.z,_PatternTex_TexelSize.z) *_PatternTex_TexelSize.x,
modFunction(i.uv.y*_MainTex_TexelSize.w,_PatternTex_TexelSize.w) *_PatternTex_TexelSize.y
)
);
fixed4 col = curColor * pattern;
col.rgb *= col.a;
return col;
}