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

Unity中使用LineRenderer在球体上绘制曲线的技术问题

在Unity中实现类似GPlates的地球仪点击绘制功能

我想实现类似科学软件GPlates的功能:在地球仪上通过点击放置顶点来绘制多边形。尝试了ChatGPT提供的两种方法后,我自己编写了代码——因为已经在处理纹理UV坐标,就想基于此实现,但完全是盲试,不知道该对哪些向量执行Slerp才能让LineRenderer画出正确的球面线条。

附上我的代码:

using UnityEngine;
using System.Collections.Generic;

public class MousePosition : MonoBehaviour
{
    public LineRenderer lineRenderer;
    public float radius = 1.0f; // 球体半径
    //public Vector2[] geoCoordinates; // 经纬度点数组(纬度,经度)
    public int numSegments = 100; // 线条平滑度分段数

    private Vector3 prevHit;
    
    void DrawCurvedLineOnSphere(Vector3 startPoint, Vector3 endPoint)
    {
        List<Vector3> linePoints = new List<Vector3>();

        // 使用Slerp沿大圆路径插值
        for (int j = 0; j <= numSegments; j++)
        {
            float t = (float)j / (float)numSegments;
            Vector3 interpolatedPoint = Vector3.Slerp(startPoint, endPoint, t);
            linePoints.Add(interpolatedPoint);
        }

        // 设置LineRenderer的点
        lineRenderer.positionCount = linePoints.Count;
        lineRenderer.SetPositions(linePoints.ToArray());
    }

    Vector3 ConvertGeoToCartesian(float latitude, float longitude, float radius)
    {
        // 角度转弧度
        float latRad = Mathf.Deg2Rad * latitude;
        float lonRad = Mathf.Deg2Rad * longitude;

        // 计算笛卡尔坐标
        float x = radius * Mathf.Cos(latRad) * Mathf.Cos(lonRad);
        float y = radius * Mathf.Sin(latRad);
        float z = radius * Mathf.Cos(latRad) * Mathf.Sin(lonRad);

        return new Vector3(x, y, z);
    }
    
    void Update() {
        if (Input.GetMouseButton(0)) {
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;

            if (Physics.Raycast(ray, out hit)) {
                Renderer renderer = hit.collider.GetComponent<Renderer>();
                Material mat = renderer.material;
                Texture2D texture2D = mat.mainTexture as Texture2D;

                // 将命中的纹理坐标转为像素坐标
                Vector2 pixelUV = hit.textureCoord;
                pixelUV.x *= texture2D.width;
                pixelUV.y *= texture2D.height;
                Debug.Log("x: " + pixelUV.x + " y: " + pixelUV.y);

                // 修改像素前检查纹理是否可读
                if (texture2D.isReadable) {
                    // 设置像素颜色
                    texture2D.SetPixel((int)pixelUV.x, (int)pixelUV.y, Color.red);
                    texture2D.Apply();
                    Debug.Log("像素颜色设置成功!");
                } else {
                    Debug.LogError("纹理不可读,请调整导入设置。");
                }
                
                if (prevHit != null) {
                    DrawCurvedLineOnSphere(prevHit, ConvertGeoToCartesian(pixelUV.x, pixelUV.y, radius));
                }
                prevHit = ConvertGeoToCartesian(pixelUV.x, pixelUV.y, radius);
            }
        }
    }
}

当前遇到的问题:

  • 我在Blender制作的球体放在世界原点,x/y/z缩放都是100,radius设为1.0是否合适?
  • 代码只能在按住鼠标拖拽时绘制一条延伸到无穷远且与球体相交的线条(LineRenderer的Alignment设为Transform Z),目标是通过两次点击完成单条线条绘制,后续点击继续添加顶点形成多边形。
  • 想最终存储经纬度而非世界空间坐标,但对UV转经纬度的坐标逻辑完全困惑——目前直接把UV像素坐标当成经纬度传入转换函数,明显错误。
  • 射线检测看似命中球体另一侧:能成功设置像素颜色,但颜色出现在球体对面位置。

解决方案

1. 球体半径参数修正

你的球体缩放为100,所以radius应设为100而非1.0。ConvertGeoToCartesian计算的是单位球坐标,乘以半径后要匹配球体实际缩放尺寸,否则生成的点会缩在原点附近,导致LineRenderer显示异常。

2. 修复点击绘制逻辑(两次点击画一条线)

将按住拖拽的逻辑改为单次点击记录起点、再次点击绘制线条:

  • 新增状态变量区分是否已记录起点
  • 改用Input.GetMouseButtonDown(0)检测单次点击(GetMouseButton是按住持续触发)

修改后的核心Update逻辑:

private bool hasStartPoint = false;
private Vector3 startCartesianPoint; // 存储起点的笛卡尔坐标

void Update() {
    if (Input.GetMouseButtonDown(0)) {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        RaycastHit hit;

        if (Physics.Raycast(ray, out hit)) {
            // 纹理像素设置部分保留

            // 将UV坐标转为正确经纬度,再转笛卡尔坐标
            Vector2 geoCoord = ConvertUVToGeo(hit.textureCoord);
            Vector3 currentCartesian = ConvertGeoToCartesian(geoCoord.x, geoCoord.y, radius);

            if (!hasStartPoint) {
                // 第一次点击,记录起点
                startCartesianPoint = currentCartesian;
                hasStartPoint = true;
            } else {
                // 第二次点击,绘制线条并重置状态
                DrawCurvedLineOnSphere(startCartesianPoint, currentCartesian);
                hasStartPoint = false;
                // 若要绘制多边形,可在此将点加入列表,最后闭合线条
            }
        }
    }
}

3. UV坐标转经纬度的正确逻辑

UV坐标范围是(0,0)到(1,1),需要映射为标准经纬度范围:

  • 经度:UV.x从0→1对应-180°→180°
  • 纬度:UV.y从0→1对应-90°→90°

新增转换函数:

Vector2 ConvertUVToGeo(Vector2 uv)
{
    // UV.x映射为经度:0→1 → -180°→180°
    float longitude = (uv.x * 360f) - 180f;
    // UV.y映射为纬度:0→1 → -90°→90°(若纹理翻转则改为(1 - uv.y))
    float latitude = (uv.y * 180f) - 90f;
    return new Vector2(latitude, longitude);
}

注意:直接使用原始UV坐标即可,无需转为像素坐标。

4. 修复射线命中球体另一侧的问题

这是因为UV的y轴方向与纹理映射方向相反,修改ConvertUVToGeo中的纬度计算:

float latitude = ((1 - uv.y) * 180f) - 90f;

同时将LineRenderer的Alignment设置为View,确保线条始终面向相机,避免视角导致的显示异常;将线条宽度调小(比如0.1),避免覆盖球面。

5. 球面线条Slerp修正

Vector3.Slerp对单位向量插值效果最佳,若线条偏离球面,可在插值后归一化再乘以半径:

Vector3 interpolatedPoint = Vector3.Slerp(startPoint.normalized, endPoint.normalized, t) * radius;

内容的提问来源于stack exchange,提问作者Grayden Hormes

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 21:59:52