You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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/ShapeProjector Shader,拖给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_Texture Shader,拖给Sphere的MeshRenderer。
  • 在ShapeProjector组件中配置形状数据,设置不同的UV范围和对应纹理,运行游戏即可看到球体表面的纹理投射效果。

四、进阶优化建议

  • 若需要大量形状,可改用ComputeBuffer传递动态数组,避免Shader中写死固定数量的变量。
  • 球体UV存在接缝问题,可改用世界坐标判断区域,替代UV判断,提升精度。
  • 如需形状旋转、缩放,可在ShapeData中添加变换参数,在Shader中对UV做矩阵变换。

内容的提问来源于stack exchange,提问作者Martin Brůžek

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 13:14:51