【Unity3D】使用GL绘制线段

发布时间 2023-03-22 21:09:39作者: little_fat_sheep

1 前言

线段渲染器LineRenderer拖尾TrailRenderer绘制物体表面三角形网格从不同角度介绍了绘制线段的方法,本文再介绍一种新的绘制线段的方法:使用 GL 绘制线段。

​ Graphics Library(简称 GL),包含一系列类似 OpenGL 的 Immediate 模式的渲染指令,比 Graphic.DrawMesh() 更高效。GL 是立即执行的,如果在Update() 方法里调用,它们将在相机渲染前执行,相机渲染前会清空屏幕,GL 渲染效果将无法看到。通常 GL 用法是:在相机上挂脚本,并在 OnPostRender() 方法里执行(MonoBehaviour的生命周期)。GL 渲染的图像不需要 GameObject 承载,在 Hierarchy 窗口不会生成 GameObject 对象。

2 代码实现

​ LinePainter.cs

using UnityEngine;

[RequireComponent(typeof(Camera))]
public class LinePainter : MonoBehaviour {
    private Material lineMaterial; // 线段材质
    private Vector3[][] circleVertices; // 3个圆环的顶点坐标
    private Color[] colors; // 3 个圆环的颜色

    private void Start() {
        lineMaterial = new Material(Shader.Find("Hidden/Internal-Colored"));
        colors = new Color[] {Color.red, Color.green, Color.blue};
        circleVertices = new Vector3[3][];
        for (int i = 0; i < 3; i++) {
            circleVertices[i] = GetCircleLines(Vector3.up, Vector3.one, i, 20);
        }
    }

    private void OnPostRender() { // GL处理不能放在Update里
        for (int i = 0; i < 3; i++) {
            DrawLines(circleVertices[i], colors[i], Color.yellow, 0.01f);
        }
    }

    private Vector3[] GetCircleLines(Vector3 center, Vector3 radius, int axis, int num) { // 获取圆环顶点数据
        Vector3[] vertices = new Vector3[num + 1];
        float ds = Mathf.PI * 2.0f / num;
        float theta = 0;
        if (axis == 0)
        {
            for (int i = 0; i <= num; i++) {
                theta += ds;
                Vector3 vec = new Vector3(0, Mathf.Cos(theta), Mathf.Sin(theta));
                vertices[i] = new Vector3(vec.x * radius.x, vec.y * radius.y, vec.z * radius.z) + center;
            }
        } else if (axis == 1) {
            for (int i = 0; i <= num; i++) {
                theta += ds;
                Vector3 vec = new Vector3(Mathf.Cos(theta), 0, Mathf.Sin(theta));
                vertices[i] = new Vector3(vec.x * radius.x, vec.y * radius.y, vec.z * radius.z) + center;
            }
        } else {
            for (int i = 0; i <= num; i++) {
                theta += ds;
                Vector3 vec = new Vector3(Mathf.Cos(theta), Mathf.Sin(theta), 0);
                vertices[i] = new Vector3(vec.x * radius.x, vec.y * radius.y, vec.z * radius.z) + center;
            }
        }
        return vertices;
    }

    private void DrawLines(Vector3[] points, Color lineColor, Color pointColor, float pointSize) { // 绘制线段
        GL.PushMatrix();
        GL.LoadIdentity();
        GL.MultMatrix(GetComponent<Camera>().worldToCameraMatrix);
        GL.LoadProjectionMatrix(GetComponent<Camera>().projectionMatrix);
        lineMaterial.SetPass(0);
        AddLines(points, lineColor);
        if (pointColor != null && pointSize > 0) {
            AddPoints(points, pointColor, pointSize);
        }
        GL.PopMatrix();
    }

    private void AddLines(Vector3[] points, Color color) { // 添加线段端点
        GL.Begin(GL.LINE_STRIP);
        GL.Color(color);
        foreach (Vector3 point in points)
        {
            GL.Vertex3(point.x, point.y, point.z);
        }
        GL.End();
    }

    private void AddPoints(Vector3[] points, Color color, float size) { // 添加绘制点(通过绘制小立方模拟顶点)
        GL.Begin(GL.QUADS);
        GL.Color(color);
        foreach (Vector3 point in points) {
            // 前面
            GL.Vertex3(point.x + size, point.y + size, point.z - size);
            GL.Vertex3(point.x + size, point.y - size, point.z - size);
            GL.Vertex3(point.x - size, point.y - size, point.z - size);
            GL.Vertex3(point.x - size, point.y + size, point.z - size);
            // 后面
            GL.Vertex3(point.x + size, point.y + size, point.z + size);
            GL.Vertex3(point.x + size, point.y - size, point.z + size);
            GL.Vertex3(point.x - size, point.y - size, point.z + size);
            GL.Vertex3(point.x - size, point.y + size, point.z + size);
            // 上面
            GL.Vertex3(point.x + size, point.y + size, point.z + size);
            GL.Vertex3(point.x + size, point.y + size, point.z - size);
            GL.Vertex3(point.x - size, point.y + size, point.z - size);
            GL.Vertex3(point.x - size, point.y + size, point.z + size);
            // 下面
            GL.Vertex3(point.x + size, point.y - size, point.z + size);
            GL.Vertex3(point.x + size, point.y - size, point.z - size);
            GL.Vertex3(point.x - size, point.y - size, point.z - size);
            GL.Vertex3(point.x - size, point.y - size, point.z + size);
            // 左面
            GL.Vertex3(point.x - size, point.y + size, point.z + size);
            GL.Vertex3(point.x - size, point.y + size, point.z - size);
            GL.Vertex3(point.x - size, point.y - size, point.z - size);
            GL.Vertex3(point.x - size, point.y - size, point.z + size);
            // 右面
            GL.Vertex3(point.x + size, point.y + size, point.z + size);
            GL.Vertex3(point.x + size, point.y + size, point.z - size);
            GL.Vertex3(point.x + size, point.y - size, point.z - size);
            GL.Vertex3(point.x + size, point.y - size, point.z + size);
        }
        GL.End();
    }
}

​ 说明: LinePainter 脚本组件需要挂在相机下。

​ SceneController.cs

using UnityEngine;
 
public class SceneController : MonoBehaviour {
    private Transform cam; // 相机
    private float nearPlan; // 近平面
    private Vector3 preMousePos; // 上一帧的鼠标坐标
    private int keyStatus = 0; // 鼠标样式状态
    private bool isDraging = false; // 是否在拖拽中
 
    void Start() {
        cam = Camera.main.transform;
        nearPlan = Camera.main.nearClipPlane;
    }
 
    void Update() {
        keyStatus = GetKeyStatus();
        UpdateScene(); // 更新场景(Ctrl+Scroll: 缩放场景, Ctrl+Drag: 平移场景, Alt+Drag: 旋转场景)
    }
 
    private int GetKeyStatus() { // 获取按键状态(0: 默认, 1: 缩放或平移, 2: 旋转)
        if (isDraging) {
            return keyStatus;
        }
        if (Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.LeftControl)) {
            return 1;
        }
        if (Input.GetKey(KeyCode.LeftAlt) || Input.GetKey(KeyCode.LeftAlt)) {
            return 2;
        }
        return 0;
    }
 
    private void UpdateScene() { // 更新场景(Ctrl+Scroll: 缩放场景, Ctrl+Drag: 平移场景, Alt+Drag: 旋转场景)
        float scroll = Input.GetAxis("Mouse ScrollWheel");
        if (!isDraging && keyStatus == 1 && Mathf.Abs(scroll) > 0) { // 缩放场景
            ScaleScene(scroll);
        } else if (Input.GetMouseButtonDown(0)) {
            preMousePos = Input.mousePosition;
            isDraging = true;
        } else if (Input.GetMouseButtonUp(0)) {
            isDraging = false;
        } else if (Input.GetMouseButton(0)) {
            Vector3 offset = Input.mousePosition - preMousePos;
            if (keyStatus == 1) { // 移动场景
                MoveScene(offset);
            } else if (keyStatus == 2) { // 旋转场景
                RotateScene(offset);
            }
            preMousePos = Input.mousePosition;
        }
    }
 
    private void ScaleScene(float scroll) { // 缩放场景
        cam.position += cam.forward * scroll;
    }
 
    private void MoveScene(Vector3 offset) { // 平移场景
        cam.position -= (cam.right * offset.x / 100 + cam.up * offset.y / 100);
    }
 
    private void RotateScene(Vector3 offset) { // 旋转场景
        Vector3 rotateCenter = GetRotateCenter(0);
        cam.RotateAround(rotateCenter, Vector3.up, offset.x / 3); // 水平拖拽分量
        cam.LookAt(rotateCenter);
        cam.RotateAround(rotateCenter, -cam.right, offset.y / 5); // 竖直拖拽分量
    }
 
    private Vector3 GetRotateCenter(float planeY) { // 获取旋转中心
        if (Mathf.Abs(cam.forward.y) < Vector3.kEpsilon || Mathf.Abs(cam.position.y) < Vector3.kEpsilon)
        {
            return cam.position + cam.forward * (nearPlan + 1 / nearPlan);
        }
        float t = (planeY - cam.position.y) / cam.forward.y;
        float x = cam.position.x + t * cam.forward.x;
        float z = cam.position.z + t * cam.forward.z;
        return new Vector3(x, planeY, z);
    }
}

​ 说明: SceneController 脚本组件用于控制相机位置和姿态,便于从不同角度查看绘制的线段,其原理介绍见→缩放、平移、旋转场景

3 运行效果

img

​ 声明:本文转自【Unity3D】使用GL绘制线段