当前位置: 首页 > news >正文

【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); // 添加新的分割点}}
}

这里使用IPointerDownHandlerIPointerMoveHandlerIPointerUpHandler接口,而没有使用传统IBeginDragHandlerIDragHandlerIEndDragHandler拖拽接口的原因是,它们使用更灵敏的输入检测,响应更快,这样在绘制是才不会有延迟,不然绘制会断断续续。

2、在非UGUI(也就是2D3D环境)下执行UI事件接口

这里使用了UI事件监听接口,具体可以参考:【unity游戏开发入门到精通——UGUI】UI事件监听接口

需要注意的是要在非UGUI(也就是2D3D环境)下执行UI事件接口,需要:

  • 确保物体上有Collider2D或者Collider组件

  • 场景中需要有EventSystem对象(新建UI时会自动创建)

  • 主相机需要有Physics2DRaycaster或者PhysicsRaycaster 组件

3、挂载脚本

新增一个Cube,挂载脚本
在这里插入图片描述

4、效果

在这里插入图片描述

方案二、使用LineRenderer实现

使用GL APIGL.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,如果学习过程中遇到任何问题,也欢迎你评论私信找我。

赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注,你的每一次支持都是我不断创作的最大动力。当然如果你发现了文章中存在错误或者有更好的解决方法,也欢迎评论私信告诉我哦!
在这里插入图片描述


文章转载自:

http://NW2wKoeZ.wfysn.cn
http://0zQqFsqm.wfysn.cn
http://POWHg62v.wfysn.cn
http://EXte3Mvl.wfysn.cn
http://cb1xCPY6.wfysn.cn
http://cHqe6a1R.wfysn.cn
http://1vUlgx90.wfysn.cn
http://w01a0Dd0.wfysn.cn
http://2kvQdzro.wfysn.cn
http://IpbL4wex.wfysn.cn
http://5myFQKT6.wfysn.cn
http://CUZMlHnG.wfysn.cn
http://1FwmVgl8.wfysn.cn
http://2rzQAvFx.wfysn.cn
http://JkIM98gg.wfysn.cn
http://cdHj9wqF.wfysn.cn
http://zsi3VSf3.wfysn.cn
http://HX0sLGUW.wfysn.cn
http://owQWXrQJ.wfysn.cn
http://QjfJcE1t.wfysn.cn
http://UI4KtWpZ.wfysn.cn
http://4NuvZ07k.wfysn.cn
http://qJVFN8Q4.wfysn.cn
http://COX1uw0e.wfysn.cn
http://lSQhSCh5.wfysn.cn
http://fNJckMr4.wfysn.cn
http://X1JBneb9.wfysn.cn
http://CQqLTPjY.wfysn.cn
http://Ta67CPWB.wfysn.cn
http://L61cZUGI.wfysn.cn
http://www.dtcms.com/a/379259.html

相关文章:

  • 2025最新超详细FreeRTOS入门教程:第十三章 FreeRTOS临界区与原子操作
  • 玩转Docker | 使用Docker部署dufs文件管理工具
  • 计算机组成原理:定点乘法运算
  • PyQt5 主窗口状态栏实时显示当前路径的实现与分析
  • 利用conda打包/复刻生信环境
  • glide介绍
  • vscode 中通义灵码显示登录过期
  • 【VScode】ssh报错
  • STM32 norflash W25Q64移植FatFS
  • 【Git】版本控制-Gitee
  • Qt常见问题
  • 泛函Φ(u)驻点的方程与边界条件 / 求给定泛函驻点满足的方程及边界条件
  • 统一权限管理平台登录不了怎么办?
  • 中级统计师-统计法规-第四章 统计管理体制
  • java反射(详细教程)
  • 【Leetcode】高频SQL基础题--1327.查找拥有有效邮箱的用户
  • Redis(集群)
  • 吾爱小工具!一键屏蔽流氓软件!
  • 告别网络监控“盲区”!OpManager全新升级解锁轻量监控新纪元!
  • 实验室试管架 | 塑料、金属等多种材质与规格 | 支持多种试管尺寸 | Sigma-Aldrich
  • .net 类库生成的DLL源码混淆加密
  • 北京-测试-入职金融公司第四周-加班&未发现bug
  • Story2Board: A Training-Free Approach for Expressive Storyboard Generation论文
  • 纯`css`轻松防止滚动穿透
  • 30天Java速成计划:从零基础到能刷算法题!
  • 【点云分类】简述对pointnet和pointnet++的理解
  • 【202509新版】Hexo + GitHub Pages 免费部署个人博客|保姆级教程
  • PigX整合knife4j
  • 安全审计-Ubuntu防火墙ufw
  • 编译器的相关知识(入门时著)