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

Unity自定义台体网格顶部/底部法线异常问题求助

Unity台体网格法线异常问题解决

问题描述

我们在Unity中通过代码生成树干模型,使用以下代码创建台体网格,能生成基础3D形状,但顶部和底部的法线显示异常,导致光照效果失常——法线向上时过亮,向下时过暗。我们是按照教程编写的顶面与底面三角面逻辑,需要排查异常原因并解决。项目要求必须在Unity内实现,不能使用Blender等外部建模软件。

原始代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[ExecuteInEditMode]
public class Frustum : MonoBehaviour
{
    /*====== PUBLIC ======*/
    public float height = 1;
    public float top_radius = 0.9F;
    public float base_radius = 1F;
    public int circle_subdivisions = 30;

    /*====== PRIVATE ======*/
    private Mesh mesh;
    private Vector3[] vao;
    private int[] ibo;

    // Start is called before the first frame update
    void Start()
    {
        mesh = new Mesh();
        mesh.name = "Cone";

        this.GetComponent<MeshFilter>().mesh = mesh;

        draw();
    }

    // OnValidate is called at each public input change
    private void OnValidate() {
        draw();
    }

    private void draw() {
        ComputeFrustumVao();
        ComputeFrustumIbo();

        mesh.Clear();
        mesh.vertices = vao;
        mesh.triangles = ibo;
        mesh.RecalculateBounds();
        mesh.RecalculateTangents();
        mesh.RecalculateNormals();
       // mesh.Optimize();
    }

    private void ComputeFrustumVao()
    {
        // COMPUTE BASE VERTICES
        List<Vector3> vertices = ComputeCircleVertices(transform.position, base_radius, circle_subdivisions);
    
        // ADD TOP VERTICES
        Vector3 top_origin = transform.position + new Vector3(0, height, 0);
        vertices.AddRange(ComputeCircleVertices(top_origin, top_radius, circle_subdivisions));
        
        vao = vertices.ToArray();
    }

    private List<Vector3> ComputeCircleVertices(Vector3 origin, float radius, int nb_subdivisions) {
        List<Vector3> vao = new List<Vector3>();
        float step_angle = (2*Mathf.PI)/nb_subdivisions;

        for(int i = 0; i < nb_subdivisions; ++i){
            float current_angle = step_angle * i;
            Vector3 translate_vector = new Vector3(
                Mathf.Cos(current_angle) * radius,  // x coordinate
                0,                                  // y coordinate
                Mathf.Sin(current_angle) * radius   // z coordinate
            );

            Vector3 new_vertex = origin + translate_vector;

            vao.Add(new_vertex);
        }

        return vao;
    }

    private void ComputeFrustumIbo() {
        List<int> indices = new List<int>();
        int nb_vertices = vao.Length;
        int nb_vertices_per_circle = nb_vertices/2;
        int nb_triangles =  nb_vertices_per_circle - 2;

        // DRAW BASE
        for(int i = 0; i < nb_triangles; ++i){
            indices.Add(0);
            indices.Add(i + 1);
            indices.Add(i + 2);
        }

        // DRAW TOP
        for(int i = nb_vertices_per_circle; i < nb_vertices_per_circle + nb_triangles; ++i){
            indices.Add(nb_vertices_per_circle);
            indices.Add(i + 2);
            indices.Add(i + 1);
        }

        // DRAW SIDES
        for(int i = 0; i < nb_vertices_per_circle; ++i){
            int next_index = (i + 1) % nb_vertices_per_circle;

            indices.Add(i);                                     // BOTTOM LEFT
            indices.Add( nb_vertices_per_circle + next_index);  // TOP RIGHT
            indices.Add(next_index);                            // BOTTOM RIGHT


            indices.Add(i);                                     // BOTTOM LEFT
            indices.Add(i + nb_vertices_per_circle);            // TOP LEFT
            indices.Add(nb_vertices_per_circle + next_index);   // TOP RIGHT
        }

        ibo = indices.ToArray();
    }

    void OnDrawGizmosSelected() //Draw the markers, if the object is selected
    { 
        Gizmos.color = Color.red;
        for (int i=0; i< mesh.vertices.Length; i++){
            Gizmos.color= new Color(((float)i)/mesh.vertices.Length,0, 0);
            Vector3    worldPos = transform.TransformPoint(mesh.vertices[i]);
            Gizmos.DrawCube(worldPos, Vector3.one * 0.1f);
        }
    }
}

异常原因

问题核心在于顶面和底面三角面的顶点缠绕顺序(Winding Order)错误:

  • Unity的RecalculateNormals()方法会根据三角面顶点的顺时针/逆时针顺序计算法线方向:顶点按逆时针顺序排列时,法线指向模型外部;顺时针则指向内部。
  • 原始代码中,底面三角面是顺时针顺序,导致法线向下(指向模型内部);顶面的索引循环逻辑错误,导致法线方向也不符合预期,最终引发光照异常。

解决方案

方案1:修正三角面顶点顺序(推荐)

修改ComputeFrustumIbo()方法中的底面和顶面索引生成逻辑:

private void ComputeFrustumIbo() {
    List<int> indices = new List<int>();
    int nb_vertices = vao.Length;
    int nb_vertices_per_circle = nb_vertices/2;
    int nb_triangles =  nb_vertices_per_circle - 2;

    // 修正:底面三角面反转顶点顺序,让法线向上(模型外部)
    for(int i = 0; i < nb_triangles; ++i){
        indices.Add(0);
        indices.Add(i + 2);
        indices.Add(i + 1);
    }

    // 修正:顶面三角面用正确的索引逻辑,保持逆时针顺序,法线向上
    for(int i = 0; i < nb_triangles; ++i){
        int topCenter = nb_vertices_per_circle;
        indices.Add(topCenter);
        indices.Add(topCenter + i + 1);
        indices.Add(topCenter + i + 2);
    }

    // 侧面索引逻辑保持不变
    for(int i = 0; i < nb_vertices_per_circle; ++i){
        int next_index = (i + 1) % nb_vertices_per_circle;

        indices.Add(i);                                     // BOTTOM LEFT
        indices.Add( nb_vertices_per_circle + next_index);  // TOP RIGHT
        indices.Add(next_index);                            // BOTTOM RIGHT


        indices.Add(i);                                     // BOTTOM LEFT
        indices.Add(i + nb_vertices_per_circle);            // TOP LEFT
        indices.Add(nb_vertices_per_circle + next_index);   // TOP RIGHT
    }

    ibo = indices.ToArray();
}

修改后,RecalculateNormals()会自动计算出正确的外部法线,光照效果恢复正常。

方案2:手动指定法线(彻底控制)

如果自动计算仍有问题,可以跳过RecalculateNormals(),手动生成法线数组:
修改draw()方法:

private void draw() {
    ComputeFrustumVao();
    ComputeFrustumIbo();

    mesh.Clear();
    mesh.vertices = vao;
    mesh.triangles = ibo;
    mesh.RecalculateBounds();
    mesh.RecalculateTangents();

    // 手动生成法线
    Vector3[] normals = new Vector3[vao.Length];
    int half = vao.Length / 2;

    // 底面法线全部向上
    for(int i = 0; i < half; i++){
        normals[i] = Vector3.up;
    }
    // 顶面法线全部向上
    for(int i = half; i < vao.Length; i++){
        normals[i] = Vector3.up;
    }
    // 侧面法线计算:垂直于侧边和底边的方向
    for(int i = 0; i < half; i++){
        int next = (i + 1) % half;
        Vector3 bottomEdge = vao[next] - vao[i];
        Vector3 sideEdge = vao[i + half] - vao[i];
        normals[i] = Vector3.Cross(bottomEdge, sideEdge).normalized;
        normals[i + half] = normals[i];
    }

    mesh.normals = normals;
    // 注释掉自动计算法线的代码
    // mesh.RecalculateNormals();
}

这种方式可以完全控制每个顶点的法线方向,避免自动计算的不确定性。

内容的提问来源于stack exchange,提问作者Jordan Roberty

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 17:05:14