Unity中如何用C#脚本实现自定义UV映射与形状纹理投射?
Unity中用C#脚本实现自定义形状投射到Mesh(含平面/球体示例)
一、核心实现思路
本质是通过C#脚本定义形状的UV范围、颜色/纹理数据,传递给Shader后,让Shader在Mesh表面逐像素判断当前UV是否在目标形状范围内,进而渲染对应颜色或纹理。这种方式无需修改Mesh本身的拓扑结构,灵活度更高。
二、平面Mesh的基础实现(4个彩色方块)
1. C#脚本:生成Mesh并传递形状数据
创建脚本生成简单平面Mesh,同时定义可配置的形状数据并传递给材质:
using UnityEngine; [System.Serializable] public class ShapeData { public Vector2 minUV; // 方块左下角UV坐标 public Vector2 maxUV; // 方块右上角UV坐标 public Color color; // 方块填充颜色 } [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] public class ShapeProjector : MonoBehaviour { public int shapeCount = 4; public ShapeData[] shapes; void Start() { GeneratePlaneMesh(); UpdateShapeDataToMaterial(); } void GeneratePlaneMesh() { Mesh mesh = new Mesh(); // 平面顶点坐标(单位大小,z轴为0) Vector3[] vertices = new Vector3[] { new(-0.5f, -0.5f, 0), new(0.5f, -0.5f, 0), new(0.5f, 0.5f, 0), new(-0.5f, 0.5f, 0) }; // 三角形索引(两个三角形组成四边形) int[] triangles = new int[] { 0, 1, 2, 0, 2, 3 }; // UV坐标(覆盖0-1范围,对应平面区域) Vector2[] uv = new Vector2[] { new(0, 0), new(1, 0), new(1, 1), new(0, 1) }; mesh.vertices = vertices; mesh.triangles = triangles; mesh.uv = uv; mesh.RecalculateNormals(); GetComponent<MeshFilter>().mesh = mesh; } void UpdateShapeDataToMaterial() { Material mat = GetComponent<MeshRenderer>().material; mat.SetInt("_ShapeCount", shapeCount); // 逐个传递形状数据到Shader for (int i = 0; i < shapeCount; i++) { mat.SetVector($"_ShapeMinUV_{i}", shapes[i].minUV); mat.SetVector($"_ShapeMaxUV_{i}", shapes[i].maxUV); mat.SetColor($"_ShapeColor_{i}", shapes[i].color); } } // 编辑器修改数据后实时更新 void OnValidate() { if (Application.isPlaying) UpdateShapeDataToMaterial(); } }
2. Shader:接收数据并渲染形状
创建Unlit类型Shader,实现逐像素的形状判断与颜色渲染:
Shader "Custom/ShapeProjector" { Properties { _MainTex ("Default Texture", 2D) = "white" {} _ShapeCount ("Shape Count", Int) = 4 } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; int _ShapeCount; // 定义4组形状数据(对应示例的4个方块) float4 _ShapeMinUV_0, _ShapeMaxUV_0; float4 _ShapeColor_0; float4 _ShapeMinUV_1, _ShapeMaxUV_1; float4 _ShapeColor_1; float4 _ShapeMinUV_2, _ShapeMaxUV_2; float4 _ShapeColor_2; float4 _ShapeMinUV_3, _ShapeMaxUV_3; float4 _ShapeColor_3; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv); // 遍历所有形状,判断当前UV是否在形状范围内 for (int j = 0; j < _ShapeCount; j++) { float2 minUV, maxUV; float4 shapeColor; switch(j) { case 0: minUV = _ShapeMinUV_0.xy; maxUV = _ShapeMaxUV_0.xy; shapeColor = _ShapeColor_0; break; case 1: minUV = _ShapeMinUV_1.xy; maxUV = _ShapeMaxUV_1.xy; shapeColor = _ShapeColor_1; break; case 2: minUV = _ShapeMinUV_2.xy; maxUV = _ShapeMaxUV_2.xy; shapeColor = _ShapeColor_2; break; case 3: minUV = _ShapeMinUV_3.xy; maxUV = _ShapeMaxUV_3.xy; shapeColor = _ShapeColor_3; break; default: continue; } bool inShape = (i.uv.x >= minUV.x && i.uv.x <= maxUV.x) && (i.uv.y >= minUV.y && i.uv.y <= maxUV.y); if (inShape) { col = shapeColor; // 覆盖默认颜色,也可用lerp实现混合 break; } } return col; } ENDCG } } }
3. 操作步骤
- 创建空GameObject,挂载
ShapeProjector脚本。 - 创建材质,选择
Custom/ShapeProjectorShader,拖给GameObject的MeshRenderer。 - 在Inspector面板的
ShapeProjector组件中,设置Shape Count为4,逐个填写形状数据:- 形状1:minUV(0,0)、maxUV(0.5,0.5)、颜色红色
- 形状2:minUV(0.5,0)、maxUV(1,0.5)、颜色绿色
- 形状3:minUV(0,0.5)、maxUV(0.5,1)、颜色蓝色
- 形状4:minUV(0.5,0.5)、maxUV(1,1)、颜色黄色
- 运行游戏即可看到平面上的4个彩色方块。
三、扩展到球体+纹理替换颜色
1. 核心调整点
球体默认使用经纬度UV,只需将ShapeData中的颜色替换为纹理,同时修改Shader的采样逻辑即可。
2. 修改C#脚本
更新ShapeData类并调整数据传递逻辑:
[System.Serializable] public class ShapeData { public Vector2 minUV; // 球体UV范围(经纬度) public Vector2 maxUV; public Texture2D texture; // 形状对应的纹理 [Range(0,1)] public float textureAlpha = 1; // 纹理混合透明度 } // 替换原UpdateShapeDataToMaterial方法 void UpdateShapeDataToMaterial() { Material mat = GetComponent<MeshRenderer>().material; mat.SetInt("_ShapeCount", shapeCount); for (int i = 0; i < shapeCount; i++) { mat.SetVector($"_ShapeMinUV_{i}", shapes[i].minUV); mat.SetVector($"_ShapeMaxUV_{i}", shapes[i].maxUV); mat.SetTexture($"_ShapeTex_{i}", shapes[i].texture); mat.SetFloat($"_ShapeTexAlpha_{i}", shapes[i].textureAlpha); } }
3. 修改Shader
更新Shader以支持纹理采样:
Shader "Custom/ShapeProjector_Texture" { Properties { _BaseTex ("Base Texture", 2D) = "white" {} _ShapeCount ("Shape Count", Int) = 4 } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _BaseTex; float4 _BaseTex_ST; int _ShapeCount; // 形状数据(含纹理) float4 _ShapeMinUV_0, _ShapeMaxUV_0; sampler2D _ShapeTex_0; float _ShapeTexAlpha_0; float4 _ShapeMinUV_1, _ShapeMaxUV_1; sampler2D _ShapeTex_1; float _ShapeTexAlpha_1; float4 _ShapeMinUV_2, _ShapeMaxUV_2; sampler2D _ShapeTex_2; float _ShapeTexAlpha_2; float4 _ShapeMinUV_3, _ShapeMaxUV_3; sampler2D _ShapeTex_3; float _ShapeTexAlpha_3; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _BaseTex); return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2D(_BaseTex, i.uv); for (int j = 0; j < _ShapeCount; j++) { float2 minUV, maxUV; sampler2D shapeTex; float alpha; switch(j) { case 0: minUV = _ShapeMinUV_0.xy; maxUV = _ShapeMaxUV_0.xy; shapeTex = _ShapeTex_0; alpha = _ShapeTexAlpha_0; break; case 1: minUV = _ShapeMinUV_1.xy; maxUV = _ShapeMaxUV_1.xy; shapeTex = _ShapeTex_1; alpha = _ShapeTexAlpha_1; break; case 2: minUV = _ShapeMinUV_2.xy; maxUV = _ShapeMaxUV_2.xy; shapeTex = _ShapeTex_2; alpha = _ShapeTexAlpha_2; break; case 3: minUV = _ShapeMinUV_3.xy; maxUV = _ShapeMaxUV_3.xy; shapeTex = _ShapeTex_3; alpha = _ShapeTexAlpha_3; break; default: continue; } bool inShape = (i.uv.x >= minUV.x && i.uv.x <= maxUV.x) && (i.uv.y >= minUV.y && i.uv.y <= maxUV.y); if (inShape) { // 用形状内的局部UV采样纹理,适配形状大小 float2 localUV = (i.uv - minUV) / (maxUV - minUV); fixed4 texCol = tex2D(shapeTex, localUV); col = lerp(col, texCol, alpha); } } return col; } ENDCG } } }
4. 球体操作步骤
- 创建Sphere GameObject,挂载
ShapeProjector脚本。 - 创建材质,选择
Custom/ShapeProjector_TextureShader,拖给Sphere的MeshRenderer。 - 在
ShapeProjector组件中配置形状数据,设置不同的UV范围和对应纹理,运行游戏即可看到球体表面的纹理投射效果。
四、进阶优化建议
- 若需要大量形状,可改用
ComputeBuffer传递动态数组,避免Shader中写死固定数量的变量。 - 球体UV存在接缝问题,可改用世界坐标判断区域,替代UV判断,提升精度。
- 如需形状旋转、缩放,可在ShapeData中添加变换参数,在Shader中对UV做矩阵变换。
内容的提问来源于stack exchange,提问作者Martin Brůžek
相关产品推荐
相关产品推荐

