【unity实战】实现在unity3D模型上画线写字涂鸦效果
文章目录
- 前言
- 方案一、使用GL绘制
- 1、代码
- 2、在非UGUI(也就是2D3D环境)下执行UI事件接口
- 3、挂载脚本
- 4、效果
- 方案二、使用LineRenderer实现
- 1、代码
- 2、效果
- 专栏推荐
- 完结
前言
本文介绍在Unity中实现3D模型表面绘制的两种技术方案。
- 方案一使用GL绘制系统,通过IPointer接口获取用户输入,将绘制点转换为模型局部坐标并沿法线方向偏移以实现立体效果,支持多段线绘制和模型旋转功能。
- 方案二采用LineRenderer组件实现。
两种方案均需在物体上添加碰撞体,并确保场景中有EventSystem和相应的PhysicsRaycaster 射线检测组件。最终效果演示了在立方体模型表面的流畅绘制过程,通过左右方向键可实时旋转模型查看绘制结果。
方案一、使用GL绘制
参考文档:https://docs.unity3d.com/ScriptReference/GL.html
1、代码
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;public class DrawOnModel : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, IPointerMoveHandler
{public Color color = Color.red; // 设置绘制线条的颜色public float thickness = 0.02f; // 设置线条的深度(沿法线方向的偏移量)private List<Vector3> points = new List<Vector3>(); // 存储绘制点的局部坐标private List<Vector3> normals = new List<Vector3>(); // 存储每个点对应的法线方向(局部坐标)private List<int> splits = new List<int>(); // 存储线条分割点的索引,用于处理多段线(如抬起画笔后重新开始绘制)// 静态材质变量,用于绘制线条static Material lineMaterial;private bool isDrawing = false; // 标记当前是否正在绘制private void Update(){// 按左右方向键,旋转模型if(Input.GetKey(KeyCode.LeftArrow)){transform.Rotate(0, 360 * Time.deltaTime, 0);}else if (Input.GetKey(KeyCode.RightArrow)){transform.Rotate(0, -360 * Time.deltaTime, 0);}}// 创建线条材质static void CreateLineMaterial(){if (!lineMaterial){// 使用Unity内置的简单着色器var shader = Shader.Find("Hidden/Internal-Colored");lineMaterial = new Material(shader);lineMaterial.hideFlags = HideFlags.HideAndDontSave;// 开启Alpha混合以实现透明效果lineMaterial.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);lineMaterial.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);// 关闭背面剔除以确保线条两面都可见lineMaterial.SetInt("_Cull", (int)UnityEngine.Rendering.CullMode.Off);// 关闭深度写入以避免深度冲突lineMaterial.SetInt("_ZWrite", 0);}}// Unity渲染回调函数,在每一帧渲染时调用public void OnRenderObject(){if (points.Count < 2) return; // 至少需要2个点才能绘制CreateLineMaterial();// 应用材质lineMaterial.SetPass(0);// 保存当前矩阵状态,并将变换矩阵设置为当前物体的局部到世界矩阵// 这样可以在物体局部空间中进行绘制GL.PushMatrix();GL.MultMatrix(transform.localToWorldMatrix);// 开始绘制线段GL.Begin(GL.LINES);int index = 0; // 分割点索引计数器for(int i = 1; i < points.Count; i++){// 如果当前点是分割点,则跳过绘制(创建新线段)if (index < splits.Count && i == splits[index]){index++;continue;}GL.Color(color); // 设置线段颜色// 计算起点:表面点 + 法线方向 * 深度var from = points[i-1] + normals[i-1] * thickness;GL.Vertex3(from.x, from.y, from.z); // 设置起点// 计算终点:表面点 + 法线方向 * 深度var to = points[i] + normals[i] * thickness;GL.Vertex3(to.x, to.y, to.z); // 设置终点}GL.End(); // 结束绘制GL.PopMatrix(); // 恢复矩阵状态}// 当鼠标/触摸按下时的回调函数public void OnPointerDown(PointerEventData eventData){isDrawing = true;}// 当鼠标/触摸移动时的回调函数public void OnPointerMove(PointerEventData eventData){if (!isDrawing) return; // 如果不是绘制状态则直接返回// 如果射线没有击中模型,则分割当前线段if (!eventData.pointerCurrentRaycast.isValid){SplitPoints();return;}// 将世界坐标转换为模型局部坐标并添加到点列表var localPosition = transform.InverseTransformPoint(eventData.pointerCurrentRaycast.worldPosition);points.Add(localPosition);// 将世界法线转换为模型局部法线并添加到法线列表var localNormal = transform.InverseTransformDirection(eventData.pointerCurrentRaycast.worldNormal);normals.Add(localNormal);}// 当鼠标/触摸抬起时的回调函数public void OnPointerUp(PointerEventData eventData){isDrawing = false;SplitPoints(); // 结束当前线段}// 分割点方法,用于标记新线段的开始void SplitPoints(){// 确保分割点列表不为空且最后一个分割点不是当前点数量// 这样可以避免重复添加分割点if ((splits.Count == 0 && points.Count != 0) || (splits.Count > 0 && splits[splits.Count - 1] != points.Count)){splits.Add(points.Count); // 添加新的分割点}}
}
这里使用IPointerDownHandler
、IPointerMoveHandler
和IPointerUpHandler
接口,而没有使用传统IBeginDragHandler
、IDragHandler
和IEndDragHandler
拖拽接口的原因是,它们使用更灵敏的输入检测,响应更快,这样在绘制是才不会有延迟,不然绘制会断断续续。
2、在非UGUI(也就是2D3D环境)下执行UI事件接口
这里使用了UI事件监听接口,具体可以参考:【unity游戏开发入门到精通——UGUI】UI事件监听接口
需要注意的是要在非UGUI(也就是2D3D环境)下执行UI事件接口,需要:
-
确保物体上有Collider2D或者Collider组件
-
场景中需要有EventSystem对象(新建UI时会自动创建)
-
主相机需要有Physics2DRaycaster或者PhysicsRaycaster 组件
3、挂载脚本
新增一个Cube,挂载脚本
4、效果
方案二、使用LineRenderer实现
使用GL API
的GL.LINES
有个致命的缺点,就是无法直接控制线条粗细,它在大多数平台上会固定为1像素宽。要实现真正可控制的线条粗细,我们需要放弃GL立即模式,转而使用其他的方法,比如使用LineRenderer
实现。
1、代码
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;public class DrawOnModelLineRenderer : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, IPointerMoveHandler
{public Color color = Color.red; // 设置绘制线条的颜色public float thickness = 0.02f; // 设置线条的厚度public float lineWidth = 0.1f; // 设置线条的宽度private List<Vector3> points = new List<Vector3>(); // 存储绘制点的局部坐标private List<Vector3> normals = new List<Vector3>(); // 存储每个点对应的法线方向(局部坐标)private List<int> splits = new List<int>(); // 存储线条分割点的索引private List<LineRenderer> lineRenderers = new List<LineRenderer>(); // 存储所有的LineRendererprivate bool isDrawing = false; // 标记当前是否正在绘制private LineRenderer currentLineRenderer; // 当前正在绘制的LineRendererprivate void Update(){// 按左右方向键,旋转模型if(Input.GetKey(KeyCode.LeftArrow)){transform.Rotate(0, 360 * Time.deltaTime, 0);}else if (Input.GetKey(KeyCode.RightArrow)){transform.Rotate(0, -360 * Time.deltaTime, 0);}}// 创建新的LineRendererprivate LineRenderer CreateNewLineRenderer(){GameObject lineObj = new GameObject("Line");lineObj.transform.SetParent(transform);lineObj.transform.localPosition = Vector3.zero;lineObj.transform.localRotation = Quaternion.identity;lineObj.transform.localScale = Vector3.one;LineRenderer lr = lineObj.AddComponent<LineRenderer>();lr.material = new Material(Shader.Find("Sprites/Default"));lr.startColor = color;lr.endColor = color;lr.startWidth = lineWidth;lr.endWidth = lineWidth;lr.useWorldSpace = false; // 使用局部空间return lr;}// 当鼠标/触摸按下时的回调函数public void OnPointerDown(PointerEventData eventData){isDrawing = true;// 创建新的LineRenderercurrentLineRenderer = CreateNewLineRenderer();lineRenderers.Add(currentLineRenderer);// 添加第一个点if (eventData.pointerCurrentRaycast.isValid){AddPoint(eventData);}}// 当鼠标/触摸移动时的回调函数public void OnPointerMove(PointerEventData eventData){if (!isDrawing) return; // 如果不是绘制状态则直接返回// 如果射线没有击中模型,则分割当前线段if (!eventData.pointerCurrentRaycast.isValid){SplitPoints();return;}AddPoint(eventData);UpdateLineRenderer();}// 添加点到当前线段private void AddPoint(PointerEventData eventData){// 将世界坐标转换为模型局部坐标并添加到点列表var localPosition = transform.InverseTransformPoint(eventData.pointerCurrentRaycast.worldPosition);points.Add(localPosition);// 将世界法线转换为模型局部法线并添加到法线列表var localNormal = transform.InverseTransformDirection(eventData.pointerCurrentRaycast.worldNormal);normals.Add(localNormal);}// 更新当前LineRenderer的顶点private void UpdateLineRenderer(){if (currentLineRenderer == null) return;// 获取当前线段的所有点(从最后一个分割点开始到当前点)int startIndex = splits.Count > 0 ? splits[splits.Count - 1] : 0;int pointCount = points.Count - startIndex;if (pointCount < 1) return;// 创建偏移后的点数组(沿法线方向偏移)Vector3[] offsetPoints = new Vector3[pointCount];for (int i = 0; i < pointCount; i++){int pointIndex = startIndex + i;offsetPoints[i] = points[pointIndex] + normals[pointIndex] * thickness;}currentLineRenderer.positionCount = pointCount;currentLineRenderer.SetPositions(offsetPoints);}// 当鼠标/触摸抬起时的回调函数public void OnPointerUp(PointerEventData eventData){isDrawing = false;SplitPoints(); // 结束当前线段currentLineRenderer = null;}// 分割点方法,用于标记新线段的开始void SplitPoints(){// 确保分割点列表不为空且最后一个分割点不是当前点数量if ((splits.Count == 0 && points.Count != 0) || (splits.Count > 0 && splits[splits.Count - 1] != points.Count)){splits.Add(points.Count); // 添加新的分割点}}// 清除所有绘制的线条public void ClearLines(){foreach (var lineRenderer in lineRenderers){if (lineRenderer != null && lineRenderer.gameObject != null){Destroy(lineRenderer.gameObject);}}lineRenderers.Clear();points.Clear();normals.Clear();splits.Clear();currentLineRenderer = null;}
}
其他设置和前面类似
2、效果
专栏推荐
地址 |
---|
【unity游戏开发入门到精通——C#篇】 |
【unity游戏开发入门到精通——unity通用篇】 |
【unity游戏开发入门到精通——unity3D篇】 |
【unity游戏开发入门到精通——unity2D篇】 |
【unity实战】 |
【制作100个Unity游戏】 |
【推荐100个unity插件】 |
【实现100个unity特效】 |
【unity框架/工具集开发】 |
【unity游戏开发——模型篇】 |
【unity游戏开发——InputSystem】 |
【unity游戏开发——Animator动画】 |
【unity游戏开发——UGUI】 |
【unity游戏开发——联网篇】 |
【unity游戏开发——优化篇】 |
【unity游戏开发——shader篇】 |
【unity游戏开发——编辑器扩展】 |
【unity游戏开发——热更新】 |
【unity游戏开发——网络】 |
完结
好了,我是向宇
,博客地址:https://xiangyu.blog.csdn.net,如果学习过程中遇到任何问题,也欢迎你评论私信找我。
赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注
,你的每一次支持
都是我不断创作的最大动力。当然如果你发现了文章中存在错误
或者有更好的解决方法
,也欢迎评论私信告诉我哦!