Unity3D绘制物体外框线条盒子

Posted little_fat_sheep

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity3D绘制物体外框线条盒子相关的知识,希望对你有一定的参考价值。

1 需求描述

        点选物体、框选物体、绘制外边框 中介绍了物体投影到屏幕上的二维外框绘制方法,本文将介绍物体外框线条盒子绘制方法。

  • 内框:选中物体后,绘制物体的内框(紧贴物体、并与物体姿态一致的内框盒子)
  • 外框:选中物体后,绘制物体的外框(紧贴物体、并与世界坐标系的朝向一致的外框盒子)

        内框和外框效果如下,其中,黄色线框是内框,绿色线框是外框。

        本文完整代码见→Unity3D绘制物体外框线条盒子

2 需求实现

        1)原理

        获取物体外框盒子(Bounds)的方法主要有:

Bounds bounds = obj.GetComponent<MeshFilter>().mesh.bounds;
Bounds bounds = obj.GetComponent<Renderer>().bounds;
Bounds bounds = obj.GetComponent<Collider>().bounds;

        MeshFilter、Render、Collider 获取的 Bounds 区别如下:

  • MeshFilter Bounds:模型原始 mesh 的 Bounds(局部坐标系下的坐标),在 Transform 组件中修改缩放,不会影响其值大小,还原其真实渲染值大小(世界坐标系下的坐标)需要通过transform.TransformPoint(vertex) 变换还原;
  • Renderer Bounds:模型渲染的真实 Bounds(世界坐标系下的坐标),其姿态与世界坐标系的坐标轴朝向保持一致,在 Transform 组件中修改缩放,会影响其值大小;
  • Collider Bounds:模型碰撞体的 Bounds(世界坐标系下的坐标),其姿态与世界坐标系的坐标轴朝向保持一致,在 Transform 组件中修改缩放,会影响其值大小,如果碰撞体与模型表面完全吻合,其 Bounds 与 Renderer 的 Bounds 保持一致。

        本文通过 MeshFilter Bounds 绘制内框盒子,通过 Renderer Bounds 绘制外框盒子。

        2)场景对象

        说明:需要删除 Plane 对象的碰撞体。 

        3)代码

        EventDetector.cs

using UnityEngine;
 
public class EventDetector : MonoBehaviour  // 事件检测器
    private MyEventType eventType = MyEventType.None; // 事件类型
    private MyEventType lastEventType = MyEventType.None; // 上次事件类型
    private float scroll; // 滑轮滑动刻度
    private bool detecting; // 事件检测中
    private Vector3 clickDownMousePos; // 鼠标按下时的坐标
    private const float dragThreshold = 1; // 识别为拖拽的鼠标偏移
 
    private void Update() 
        detecting = true;
        DetectMouseEvent();
        DetectScrollEvent();
        UpgradeMouseEvent();
        detecting = false;
        lastEventType = eventType;
    
 
    private void DetectMouseEvent()  // 检测鼠标事件
        if (Input.GetMouseButtonDown(0))  // Click Down
            eventType = MyEventType.ClickDown;
            clickDownMousePos = Input.mousePosition;
         else if (Input.GetMouseButtonUp(0)) 
            if (IsDragEvent(eventType))  // End Drag
                eventType = MyEventType.EndDrag;
             else  // Click Up
                eventType = MyEventType.ClickUp;
            
         else if (Input.GetMouseButton(0)) 
            if (IsDragEvent(eventType))  // Drag
                eventType = MyEventType.Drag;
             else if (Vector3.Distance(clickDownMousePos, Input.mousePosition) > dragThreshold)  // Begin Drag
                eventType = MyEventType.BeginDrag;
             else  // Click
                eventType = MyEventType.Click;
            
         else 
            eventType = MyEventType.None;
        
    
 
    private void DetectScrollEvent()  // 检测滑轮事件
        if (eventType != MyEventType.None
            && (!IsBeginEvent(eventType) || lastEventType != MyEventType.None && !IsScrollEvent(lastEventType))) 
            scroll = 0;
            return;
        
        float temScroll = Input.GetAxis("Mouse ScrollWheel");
        if (Mathf.Abs(scroll) < float.Epsilon && Mathf.Abs(temScroll) > float.Epsilon)  // Begin Scroll
            eventType = MyEventType.BeginScroll;
            scroll = temScroll;
         else if (Mathf.Abs(scroll) > float.Epsilon && Mathf.Abs(temScroll) < float.Epsilon)  // End Scroll
            eventType = MyEventType.EndScroll;
            scroll = temScroll;
         else if (Mathf.Abs(temScroll) > float.Epsilon)  // Scroll
            eventType = MyEventType.Scroll;
            scroll = temScroll;
         else 
            scroll = 0;
        
    
 
    private void UpgradeMouseEvent()  // 升级鼠标事件(关联键盘事件)
        if (eventType == MyEventType.None) 
            return;
        
        if (IsBeginEvent(eventType)) 
            if (Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl)) 
                AddKeyType("Ctrl");
             else if (Input.GetKey(KeyCode.LeftAlt) || Input.GetKey(KeyCode.RightAlt)) 
                AddKeyType("Alt");
            
         else 
            ContinueKeyType(); // 保持按键事件
        
    
 
    public MyEventType EventType()  // 事件类型
        if (detecting) 
            return lastEventType;
        
        return eventType;
    
 
    public bool HasClickEvent()  // 是否有点击事件
        MyEventType type = EventType();
        return IsClickEvent(type);
    
 
    public bool HasDragEvent()  // 是否有拖拽事件
        MyEventType type = EventType();
        return IsDragEvent(type);
    
 
    public bool HasScrollEvent()  // 是否有滑轮事件
        MyEventType type = EventType();
        return IsScrollEvent(type);
    

    public bool HasCtrlScrollEvent()  // 是否有Ctrl滑轮事件
        MyEventType type = EventType();
        return type >= MyEventType.BeginCtrlScroll && type <= MyEventType.EndCtrlScroll;
    
 
    public bool IsBeginDrag()  // 是否是开始拖拽类型事件
        MyEventType type = EventType();
        return type == MyEventType.BeginDrag || type == MyEventType.BeginCtrlDrag || type == MyEventType.BeginAltDrag;
    
 
    public float Scroll()  // 鼠标滑轮滑动刻度
        if (HasScrollEvent()) 
            return scroll;
        
        return 0;
    
 
    private bool IsClickEvent(MyEventType type)  // 是否是点击事件
        return type >= MyEventType.ClickDown && type <= MyEventType.CtrlClickUp;
    
 
    private bool IsDragEvent(MyEventType type)  // 是否是拖拽事件
        return type >= MyEventType.BeginDrag && type <= MyEventType.EndAltDrag;
    
 
    private bool IsScrollEvent(MyEventType type)  // 是否是滑轮事件
        return type >= MyEventType.BeginScroll && type <= MyEventType.EndCtrlScroll;
    
 
    private bool IsBeginEvent(MyEventType type)  // 是否是开始类型事件
        return type == MyEventType.ClickDown
            || type == MyEventType.BeginDrag
            || type == MyEventType.BeginCtrlDrag
            || type == MyEventType.BeginAltDrag
            || type == MyEventType.BeginScroll
            || type == MyEventType.BeginCtrlScroll;
    
 
    private bool HasCtrlKey(MyEventType type)  // 是否有Ctrl按键事件
        return type >= MyEventType.CtrlClickDown && type <= MyEventType.CtrlClickUp
            || type >= MyEventType.BeginCtrlDrag && type <= MyEventType.EndCtrlDrag
            || type >= MyEventType.BeginCtrlScroll && type <= MyEventType.EndCtrlScroll;
    
 
    private bool HasAltKey(MyEventType type)  // 是否有Alt按键事件
        return type >= MyEventType.BeginAltDrag && type <= MyEventType.EndAltDrag;
    
 
    private void ContinueKeyType()  // 保持按键事件
        if (HasCtrlKey(lastEventType)) 
            AddKeyType("Ctrl");
         else if (HasAltKey(lastEventType)) 
            AddKeyType("Alt");
        
    
 
    private void AddKeyType(string key)  // 添加按键事件
        if ("Ctrl".Equals(key)) 
            if (eventType == MyEventType.ClickDown)  // 点击事件
                eventType = MyEventType.CtrlClickDown;
             else if (eventType == MyEventType.Click) 
                eventType = MyEventType.CtrlClick;
             else if (eventType == MyEventType.ClickUp) 
                eventType = MyEventType.CtrlClickUp;
             else if (eventType == MyEventType.BeginDrag)  // 拖拽事件
                eventType = MyEventType.BeginCtrlDrag;
             else if (eventType == MyEventType.Drag) 
                eventType = MyEventType.CtrlDrag;
             else if (eventType == MyEventType.EndDrag) 
                eventType = MyEventType.EndCtrlDrag;
             else if (eventType == MyEventType.BeginScroll)  // 滑轮事件
                eventType = MyEventType.BeginCtrlScroll;
             else if (eventType == MyEventType.Scroll) 
                eventType = MyEventType.CtrlScroll;
             else if (eventType == MyEventType.EndScroll) 
                eventType = MyEventType.EndCtrlScroll;
            
         else if ("Alt".Equals(key)) 
            if (eventType == MyEventType.BeginDrag)  // 拖拽事件
                eventType = MyEventType.BeginAltDrag;
             else if (eventType == MyEventType.Drag) 
                eventType = MyEventType.AltDrag;
             else if (eventType == MyEventType.EndDrag) 
                eventType = MyEventType.EndAltDrag;
            
        
    

 
public enum MyEventType  // 事件类型
    None = 0,
    ClickDown = 1,
    Click = 2,
    ClickUp = 3,
    CtrlClickDown = 4,
    CtrlClick = 5,
    CtrlClickUp = 6,
    BeginDrag = 10,
    Drag = 11,
    EndDrag = 12,
    BeginCtrlDrag = 13,
    CtrlDrag = 14,
    EndCtrlDrag = 15,
    BeginAltDrag = 16,
    AltDrag = 17,
    EndAltDrag = 18,
    BeginScroll = 20,
    Scroll = 21,
    EndScroll = 22,
    BeginCtrlScroll = 23,
    CtrlScroll = 24,
    EndCtrlScroll = 25

        说明: EventDetector 脚本组件挂在相机下,用于统一管理事件。点选物体(ClickUp)、滑动选框(Drag)、场景变换(Ctrl + Drag / Alt + Drag)都有鼠标事件,这些事件相互冲突,不便于在每个类里都去捕获鼠标和键盘事件,因此需要 EventDetector 统一管理事件。

        ClickSelect.cs

using UnityEngine;

public class ClickSelect : MonoBehaviour 
    private EventDetector eventDetector; // 鼠标事件检测器
    private LineBoxPainder lineBoxPainder;
    private Transform target; // 选中的目标
    private RaycastHit hit; // 碰撞信息

    private void Awake() 
        eventDetector = Camera.main.GetComponent<EventDetector>();
        lineBoxPainder = LineBoxPainder.GetInstance();
    

    private void Update() 
        if (eventDetector.EventType() == MyEventType.ClickUp) 
            Transform temp = GetHitTrans();
            UpdateColor(target, temp);
            target = temp;
            if (target != null) 
                lineBoxPainder.DrawLineBox(target.gameObject);
            
            else 
                lineBoxPainder.DrawLineBox(null);
            
        
    

    private void UpdateColor(Transform old, Transform now)  // 更新颜色
        if (old != now) 
            if (old != null) 
                old.GetComponent<Renderer>().material.color = Color.gray;
            
            if (now != null) 
                now.GetComponent<Renderer>().material.color = Color.red;
            
        
    

    private Transform GetHitTrans()  // 获取屏幕射线碰撞的物体
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        if (Physics.Raycast(ray, out hit)) 
            return hit.transform;
        
        return null;
    

        说明:ClickSelect 脚本组件挂在 Work 下。

        LineBoxPainder.cs

using UnityEngine;

public class LineBoxPainder  // 线段盒子渲染器(每个线段盒子由4个矩形组成)
    private static LineBoxPainder instance; // 单例
    private GameObject lineParent; // 线条盒子父对象
    private LineRenderer[][] lineRenderers; // 线段渲染器
    private Material lineMaterial; // 线段材质

    private LineBoxPainder() 
        lineMaterial = new Material(Shader.Find("Hidden/Internal-Colored"));
        lineParent = new GameObject("LineBoxPainder");
        lineRenderers = new LineRenderer[2][];
        lineRenderers[0] = GetLineRenderers("InnerBox", Color.yellow);
        lineRenderers[1] = GetLineRenderers("OuterBox", Color.green);
    

    public static LineBoxPainder GetInstance()  // 获取单例
        if (instance == null) 
            instance = new LineBoxPainder();
        
        return instance;
    

    public void DrawLineBox(GameObject obj)  // 绘制内框盒子和外框盒子
        Vector3[] InnerVertices = BoxProvider.GetInnerBox(obj);
        DrawBox(lineRenderers[0], InnerVertices); // 绘制内框盒子
        Vector3[] OuterVertices = BoxProvider.GetOuterBox(obj);
        DrawBox(lineRenderers[1], OuterVertices); // 绘制外框盒子
    

    private LineRenderer[] GetLineRenderers(string name, Color color)  // 获取线段渲染器
        Material material = new Material(lineMaterial);
        material.color = color;
        GameObject box = new GameObject(name);
        box.transform.parent = lineParent.transform;
        LineRenderer[] lines = new LineRenderer[4];
        for (int i = 0; i < lines.Length; i++) 
            GameObject line = new GameObject("Line" + i);
            line.transform.parent = box.transform;
            lines[i] = line.AddComponent<LineRenderer>();
            lines[i].material = material;
            lines[i].textureMode = LineTextureMode.Tile;
            lines[i].widthMultiplier = 0.2f;
            lines[i].startWidth = 0.05f;
            lines[i].endWidth = 0.05f;
            lines[i].positionCount = 0;
            lines[i].loop = true;
        
        return lines;
    

    private void DrawBox(LineRenderer[] lines, Vector3[] vertices)  // 绘制一个长方体线段盒子, 每个盒子由4个矩形组成
        if (vertices == null || vertices.Length == 0) 
            for (int i = 0; i < 4; i++)  // 清空线段顶点
                lines[i].positionCount = 0;
            
            return;
        
        else
        
            for (int i = 0; i < 4; i++)  // 初始化线段顶点
                lines[i].positionCount = 4;
            
        
        for (int i = 0; i < 4; i++)  // 计算每个矩形的顶点序列
            lines[0].SetPosition(i, vertices[i]);
            lines[1].SetPosition(i, vertices[i + 4]);
            if (i < 2) 
                lines[2].SetPosition(i, vertices[i]);
                lines[3].SetPosition(i, vertices[i + 2]);
             else 
                lines[2].SetPosition(i, vertices[7 -i]);
                lines[3].SetPosition(i, vertices[9 -i]);
            
        
    

        说明:LineBoxPainder 用于绘制内框和外框线段盒子,每个盒子使用 4 个 LineRenderer 渲染(对应 4 个矩形),每个 LineRenderer 有 4 个顶点,并设置为 loop,用于渲染一个矩形,一个长方体需要 4 个矩形拼成

        BoxProvider.cs

using UnityEngine;

public class BoxProvider  // 盒子提供者
    public static Vector3[] GetInnerBox(GameObject obj)  // 获取内框盒子8个顶点数据
        if (obj == null || obj.GetComponent<MeshFilter>() == null) 
            return null;
        
        Bounds bounds = obj.GetComponent<MeshFilter>().mesh.bounds;
        Vector3[] vertices = GetBoxVertices(bounds);
        for (int i = 0; i < vertices.Length; i++)  // 将局部坐标转换为世界坐标
            vertices[i] = obj.transform.TransformPoint(vertices[i]);
        
        return vertices;
    

    public static Vector3[] GetOuterBox(GameObject obj)  // 获取外框盒子8个顶点数据
        if (obj == null || obj.GetComponent<Renderer>() == null) 
            return null;
        
        Bounds bounds = obj.GetComponent<Renderer>().bounds;
        //Bounds bounds = obj.GetComponent<Collider>().bounds;
        Vector3[] vertices = GetBoxVertices(bounds);
        return vertices;
    

    private static Vector3[] GetBoxVertices(Bounds bounds)  // 根据中心坐标和半边长计算8个顶点的数据
        Vector3 center = bounds.center;
        Vector3 extents = bounds.extents;
        Vector3[] vertices = new Vector3[8];
        vertices[0] = center + new Vector3(extents.x, extents.y, extents.z);
        vertices[1] = center + new Vector3(extents.x, extents.y, -extents.z);
        vertices[2] = center + new Vector3(extents.x, -extents.y, -extents.z);
        vertices[3] = center + new Vector3(extents.x, -extents.y, extents.z);
        vertices[4] = center + new Vector3(-extents.x, extents.y, extents.z);
        vertices[5] = center + new Vector3(-extents.x, extents.y, -extents.z);
        vertices[6] = center + new Vector3(-extents.x, -extents.y, -extents.z);
        vertices[7] = center + new Vector3(-extents.x, -extents.y, extents.z);
        return vertices;
    

        说明:BoxProvider 用于计算物体外框的 8 个顶点序列。本文通过 MeshFilter Bounds 绘制内框盒子,通过 Renderer Bounds 绘制外框盒子。

        SceneController.cs

using System;
using UnityEngine;
 
public class SceneController : MonoBehaviour  // 场景变换控制器
    private EventDetector eventDetector; // 鼠标事件检测器
    public Action camChangedHandler; // 相机改变处理器
    private Transform cam; // 相机
    private float nearPlan; // 近平面
    private Vector3 preMousePos; // 上一帧的鼠标坐标
 
    private void Awake() 
        cam = Camera.main.transform;
        nearPlan = Camera.main.nearClipPlane;
        eventDetector = cam.GetComponent<EventDetector>();
    
 
    private void Update()  // 更新场景(Ctrl+Scroll: 缩放场景, Ctrl+Drag: 平移场景, Alt+Drag: 旋转场景)
        if (eventDetector.HasCtrlScrollEvent())  // 缩放场景
            ScaleScene(eventDetector.Scroll());
         else if (eventDetector.IsBeginDrag()) 
            preMousePos = Input.mousePosition;
         else if (eventDetector.HasDragEvent()) 
            Vector3 offset = Input.mousePosition - preMousePos;
            if (eventDetector.EventType() == MyEventType.CtrlDrag)  // 移动场景
                MoveScene(offset);
             else if (eventDetector.EventType() == MyEventType.AltDrag)  // 旋转场景
                RotateScene(offset);
            
            preMousePos = Input.mousePosition;
        
    
 
    private void ScaleScene(float scroll)  // 缩放场景
        cam.position += cam.forward * scroll;
        camChangedHandler?.Invoke();
    
 
    private void MoveScene(Vector3 offset)  // 平移场景
        cam.position -= (cam.right * offset.x / 100 + cam.up * offset.y / 100);
        camChangedHandler?.Invoke();
    
 
    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); // 竖直拖拽分量
        camChangedHandler?.Invoke();
    
 
    private Vector3 GetRotateCenter(float planeY)  // 获取旋转中心
        if (Mathf.Abs(cam.forward.y) < Vector3.kEpsilon || Mathf.Abs(cam.position.y) < float.Epsilon)
        
            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 运行效果

4 拓展

        本节主要介绍长方体盒子的信息解析,主要解决以下 2 个问题:

  • 已知长方体顶点坐标,求长方体中心坐标、尺寸、旋转角度;

  • 已知长方体中心坐标、尺寸、旋转角度,求长方体顶点坐标。

        BoxParser.cs

using UnityEngine;

public class BoxParser  // 解析盒子信息
    // 已知长方体顶点坐标, 获取长方体中心坐标、尺寸、旋转角度
    // 输入的顶点顺序: 右上前、右上后、右下后、右下前、左上前、左上后、左下后、左下前
    public static void GetBoxInfo(Vector3[] vertices, out Vector3 center, out Vector3 extents, out Vector3 rotation) 
        center = (vertices[0] + vertices[6]) / 2;
        float sizeX = Vector3.Distance(vertices[0], vertices[4]); // 上前棱长
        float sizeY = Vector3.Distance(vertices[0], vertices[3]); // 右前棱长
        float sizeZ = Vector3.Distance(vertices[0], vertices[1]); // 右上棱长
        extents = new Vector3(sizeX, sizeY, sizeZ) / 2;
        Vector3 forward = (vertices[0] + vertices[7]) / 2 - center; // 本地向前的向量
        Vector3 up = (vertices[0] + vertices[5]) / 2 - center; // 本地向上的向量
        Quaternion qua = Quaternion.LookRotation(forward, up);
        rotation = qua.eulerAngles;
    

    // 已知长方体中心坐标、尺寸、旋转角度, 获取长方体顶点坐标
    public static Vector3[] GetVertices(Vector3 center, Vector3 extents, Vector3 rotation) 
        Vector3[] vertices = GetInitVertices(extents);
        RotateAndTranslate(vertices, rotation, center);
        return vertices;
    

    private static Vector3[] GetInitVertices(Vector3 extents)  // 根据半边长计算8个顶点的数据
        // 输出的顶点顺序: 右上前、右上后、右下后、右下前、左上前、左上后、左下后、左下前
        Vector3[] vertices = new Vector3[8];
        vertices[0] = new Vector3(extents.x, extents.y, extents.z);
        vertices[1] = new Vector3(extents.x, extents.y, -extents.z);
        vertices[2] = new Vector3(extents.x, -extents.y, -extents.z);
        vertices[3] = new Vector3(extents.x, -extents.y, extents.z);
        vertices[4] = new Vector3(-extents.x, extents.y, extents.z);
        vertices[5] = new Vector3(-extents.x, extents.y, -extents.z);
        vertices[6] = new Vector3(-extents.x, -extents.y, -extents.z);
        vertices[7] = new Vector3(-extents.x, -extents.y, extents.z);
        return vertices;
    

    private static void RotateAndTranslate(Vector3[] vertices, Vector3 rotation, Vector3 center)  // 旋转和平移
        Quaternion qua = Quaternion.Euler(rotation.x, rotation.y, rotation.z);
        Matrix4x4 matrix = Matrix4x4.Rotate(qua);
        for (int i = 0; i < vertices.Length; i++)  // 将局部坐标转换为世界坐标
            vertices[i] = center + matrix.MultiplyPoint(vertices[i]);
        
    

C#wpf里面怎么绘制线条

如果是绘制单根直线,那么使用Line类。
Line类继承自Shape,Shape继承自FrameworkElement,FrameworkElement继承自UIElement,所以Panel可以直接调用.Children.Add()方法添加Line。
首先在Window中添加一个Canvas,名字是canvas1,那么添加直线的代码就是
Line myLine = new Line();
myLine = new Line();
myLine.Stroke = System.Windows.Media.Brushes.LightSteelBlue;
myLine.X1 = 1;
myLine.X2 = 50;
myLine.Y1 = 1;
myLine.Y2 = 50;
myLine.HorizontalAlignment = HorizontalAlignment.Left;
myLine.VerticalAlignment = VerticalAlignment.Center;
this.canvas1.Children.Add(myLine);

其中
myLine.Stroke = System.Windows.Media.Brushes.LightSteelBlue;
很重要,用来选择画刷。如果没有的话话出来的线就是白色的。
另外
myLine.StrokeThickness = 2;

是用来控制画刷的粗细的。
参考技术A XAML写
<Line X1="100" X2="200" Y1="100" Y2="200"/>
就可以

以上是关于Unity3D绘制物体外框线条盒子的主要内容,如果未能解决你的问题,请参考以下文章

HTML5+CSS——11盒子模型-边框、内边距、外边距

盒子模型

9.求背景图片左边到#box盒子左边外框侧的距离

word中制作的表格线条是灰色的,不能打印,怎么设置黑色的?

box-sizing重置盒子模型计算规则

Matplotlib:如何在非透明线边缘处拥有透明的盒子图?