ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AR涂色开发实战:UV实时绘制与图像跟踪全解析

AR涂色开发实战:UV实时绘制与图像跟踪全解析 简介面向Unity开发者的AR涂色项目资源包适用于想借助EasyAR实现图像识别与实时上色交互的开发者。内含Unity工程运行时需要的核心文件以zip压缩包发布共545个文件、约35.3MB其中165个meta维护资源引用关系41个cs脚本承担触摸选色、颜色更新与识别回调等逻辑13个prefab搭好可复用对象15个mat和7个shader负责模型外观呈现再加上dll、xml、png等辅助资源基本覆盖从目标识别、对象叠加到画面渲染的完整环节。目前已有1489人学习下载。通过阅读场景配置、EasyAR接入文件与着色器设置可梳理黑白线稿识别、3D对象生成、颜色实时映射的具体调用路径同时借助脚本结构还能快速定位关键函数适合在熟悉Unity基础操作后对照复现也可作为自定义AR涂色玩法的改造模板或在此基础上扩展颜色选择、橡皮擦等交互功能。1. AR 涂色不只是把调色板搬进屏幕它到底解决什么问题AR 涂色技术是把一张普通线稿纸变成 AR 交互入口的整套实现用户打印或贴一张参考图打开 App 对准3D 模型就在纸面上浮现手指一划颜色实时盖到模型表面转动纸片还能看到立体效果。它和普通换装、捏脸的区别在于创作入口在物理世界反馈在 3D 空间适合儿童益智、实体玩具互动、线下门店引流这类场景。本文面向要自己动手做 AR 涂色的开发者和技术负责人会从架构选型讲到 UV 绘制实现再列出真正拖垮体验的几个坑。看完你至少能把第一版最小工程跑通并且知道接下来在哪调参。2. 先定架构图像跟踪、模型对齐和三条上色路线的取舍做 AR 涂色的第一步不是写涂色逻辑而是把“纸在哪、模型贴在哪”这件事先定下来。整个链路可以拆成三段图像跟踪锁定纸张位置模型跟纸对齐用户触摸模型时把颜色写进纹理。架构上我建议直接用 AR Foundation它把 ARCore 和 ARKit 的底层能力统一了图像跟踪和相机姿态都不用自己写视觉算法团队可以集中精力处理“颜色怎么样才不像假的”。2.1 三条上色路线贴图替换、UV 实时绘制、顶点色为什么选 UV“上色”在 Unity 里至少有三种做法选错后面改起来很痛苦我把它们拆开说清楚。第一种是贴图替换。预先给模型烘焙 N 套颜色纹理用户选择后直接替换材质的 MainTexture。优点是实现最简单、性能最好、兼容设备广缺点是用户没有“涂”的过程只是“换”。它适合换装不适合涂色产品。第二种是顶点色。把颜色写进 Mesh 的顶点颜色数组渲染时 Shader 采样 vertexColor。优点是不依赖纹理分辨率、内存占用小缺点是颜色过渡是顶点之间的硬过渡模型细分不够时涂出来一棱一棱的而且想做柔和笔刷边缘非常吃力笔画基本调不出圆滑效果。第三种是 UV 实时绘制。运行时维护一张 Texture2D用户触摸模型上的位置把 RaycastHit 里的 UV 坐标换算成纹理像素坐标用 SetPixel / SetPixels 把颜色写进去再 Apply 到材质上。它保留了真实的“画”的过程能表达笔触、叠色和覆盖坏处是性能要自己控制好UV 展开不均匀时会觉得在某些区域笔刷忽大忽小。三条路线的取舍我实际做下来是这样判断的方案交互自由性能实现成本适合场景贴图替换低只能换预设优低换装、选色顶点色中笔触生硬良中低清模型、内嵌渲染UV 实时绘制高自由涂画可控较高涂色书、玩具互动儿童涂色产品用户要的就是“自己画”所以 UV 实时绘制是默认选择。哪怕一开始原型做贴图替换我建议也把纹理绘制预留好不然交互评审时一定会被要求加上。2.2 用 AR Foundation 的 Image Tracking 锁住一张涂色纸图像跟踪是 AR 涂色的地基。原理不复杂手机相机识别到预设参考图SDK 返回它的位置和姿态Unity 把模型挂在这个坐标系下。ARTrackedImageManager 是 AR Foundation 里做这件事的入口参考图可以提前放进图库也可以在运行时动态注册。动态注册我经常用因为涂色页可能有好几种排版运行时按需加更灵活。using UnityEngine.XR.ARFoundation; using UnityEngine.XR.ARSubsystems; public class ColoringPageRegister : MonoBehaviour { public ARTrackedImageManager trackedImageManager; public Texture2D pageTexture; // 涂色纸的纹理 public float printedWidthMeters 0.25f; // 打印出来的实际宽度单位米 void Awake() { if (trackedImageManager.referenceLibrary null) { // 运行时创建参考图库适合在启动后动态加载涂色页 var library ARImageLibrary.CreateRuntimeLibrary(); library.AddTexture(pageTexture, coloring_page, printedWidthMeters); trackedImageManager.referenceLibrary library; } } void OnEnable() { trackedImageManager.trackedImagesChanged OnTrackedImagesChanged; } void OnDisable() { trackedImageManager.trackedImagesChanged - OnTrackedImagesChanged; } void OnTrackedImagesChanged(ARTrackedImagesChangedEventArgs args) { foreach (var image in args.added) { // 模型作为纸张的子节点位置自然跟随 var model Instantiate(modelPrefab, image.transform); model.transform.localPosition new Vector3(0f, 0.02f, 0f); } } }这段代码有几个参数不能瞎填。printedWidthMeters 必须是用户实际打印出来的纸张宽度填错了模型大小和纸张比例会对不上。添加参考图时 name 要唯一图像纹理不要带过分复杂的背景。还有一个容易忽略的地方不要把模型挂到 AR Session Origin 下挂到 image.transform 下才能保证每次重新识别到纸时模型自动回到正确位置。2.3 模型对齐让 3D 线稿和纸上轮廓粘住不掉模型挂上去了不代表视觉上对齐还需要处理两个参数。第一个是模型的默认尺寸。建模时 Unity 场景里 1 个单位通常等于 1 米但涂色纸可能只有 20 厘米宽模型放进纸面上就过大。最简单的做法是给模型根节点一个 Scale 校准值在真机上拍照微调我一般写成可配置字段而不是直接改美术资源。第二个是偏移和旋转。纸张本身是平整放在桌上的模型底部如果没对准纸面中心会出现“悬空”或者“陷进桌面”。ARTrackedImage 的 transform 原点在图像中心所以模型根节点的原点也最好建模在底座中心。如果模型没有底座用代码临时把 Y 轴偏移一点让视觉重心落在纸上即可。这里还要提一下相机跟随AR 场景里相机移动主要靠用户手持设备Unity 的 Camera 组件保持默认不动模型位置跟随纸张变化所以看起来画面始终稳定。不要给相机写死跟随脚本那样在跟踪丢失重识别时很容易镜头跳变。3. 把颜色实际画上去UV 实时绘制的最小可运行实现架构确定后接下来是涂色系统的核心把用户的触摸转换成纹理上的笔迹。这里我先说一个前提给模型的 Collider 必须是 MeshCollider而不是 BoxCollider。因为 RaycastHit.textureCoord 只有碰到 MeshCollider 才会返回有意义的 UV 值这是最容易翻车的前置条件。3.1 触摸到 UV一次笔触的完整链路用户手指按在屏幕某点Unity 用相机发射一条射线击中模型后拿到一个 RaycastHithit.textureCoord 就是模型表面那个点的 UV 坐标。UV 通常归一化到 0 到 1画到 1024 宽的纹理上就乘以 1024 得到像素坐标。接着在像素坐标周围画一个圆形的笔迹颜色写入纹理Apply 回 GPU。做完这些材质上就会出现一个笔点。这里有个容易被忽略的点UV 坐标的 V 轴和纹理像素坐标的 Y 轴可能方向相反。如果画上去的笔迹位置上下颠倒可以在换算像素坐标时做一次y textureHeight - (int)(uv.y * textureHeight)修正。3.2 最小绘制代码与参数说明下面这段代码是我保留过的最简版本没有做插值平滑但已经能跑通完整链路。笔刷半径用一个可调字段控制透明度用 Color.Lerp 模拟叠色效果。using UnityEngine; public class ARPainter : MonoBehaviour { public Camera arCamera; public Material targetMaterial; [Range(2, 64)] public int brushRadiusPx 24; [Range(0.05f, 1f)] public float brushOpacity 0.6f; public Color brushColor Color.red; private Texture2D _paintTexture; private const int TextureSize 1024; void Start() { // 创建可写纹理RGBA32 便于转 PNG 导出 _paintTexture new Texture2D(TextureSize, TextureSize, TextureFormat.RGBA32, false); _paintTexture.filterMode FilterMode.Bilinear; _paintTexture.wrapMode TextureWrapMode.Clamp; targetMaterial.mainTexture _paintTexture; } void Update() { // 单点触摸涂色场景暂不考虑双指手势 if (Input.touchCount ! 1) return; Touch touch Input.GetTouch(0); if (touch.phase ! TouchPhase.Began touch.phase ! TouchPhase.Moved) return; Ray ray arCamera.ScreenPointToRay(touch.position); if (!Physics.Raycast(ray, out RaycastHit hit, 5f)) return; Vector2 uv hit.textureCoord; PaintAtUV(uv); } void PaintAtUV(Vector2 uv) { int cx Mathf.FloorToInt(uv.x * _paintTexture.width); int cy Mathf.FloorToInt(uv.y * _paintTexture.height); // 圆形笔刷逐个像素写入 for (int x cx - brushRadiusPx; x cx brushRadiusPx; x) { for (int y cy - brushRadiusPx; y cy brushRadiusPx; y) { if (x 0 || x _paintTexture.width) continue; if (y 0 || y _paintTexture.height) continue; float dist Vector2.Distance(new Vector2(x, y), new Vector2(cx, cy)); if (dist brushRadiusPx) continue; Color old _paintTexture.GetPixel(x, y); Color merged Color.Lerp(old, brushColor, brushOpacity); _paintTexture.SetPixel(x, y, merged); } } _paintTexture.Apply(false); } }这段代码逻辑很直观但参数有三处值得留意。brushRadiusPx 是像素半径在 1024 纹理下 24 像素大约是直径 48 像素视觉上是比较细的笔。如果模型只有巴掌大24 像素可能已经显得很粗要按实际模型缩放来调。brushOpacity 控制单次笔迹的不透明度0.6 会让叠色时颜色逐渐加深适合模拟蜡笔质感如果设成 1 就变成完全覆盖了。纹理分辨率固定 1024 是性能和细节的折中2048 会更细腻但低端机会明显掉帧。另外代码里的Apply(false)第二个参数是makeNoLongerReadable设成 false 表示还需要继续读取像素。如果设成 true后续 GetPixel 就读不出来了这个坑我踩过一次画到一半纹理直接变黑盒。3.3 调色板、笔刷大小和笔迹融合三个交互参数代码能跑起来之后要打磨的是交互手感。调色板我建议用独立 Canvas 做在屏幕底部点击颜色时只改 brushColor不暂停绘制。不要把调色板做成模型的一部分否则手指选色时会遮挡模型和纸张的交互区域。笔刷大小在真实项目里要能调最好做成滑块。不同年龄段用户差异很大儿童经常一笔划很长笔刷半径小了笔迹断断续续显眼成年人更在意细节6 到 12 像素半径才合适。给一个 Range 属性然后暴露在 Inspector 里真机调试时摸摸看比写死在代码里实用。笔迹融合是新手最容易忽略掉的体验点。上面的 Color.Lerp 是线性融合笔迹没有边缘渐变放大看像硬贴上去的圆片。想要柔和边缘可以在循环里加一个基于 dist 的衰减权重float alpha brushOpacity * (1f - dist / brushRadiusPx); Color merged Color.Lerp(old, brushColor, alpha);这样中心实、边缘虚画出来更接近水彩或蜡笔。但要注意 alpha 衰减后颜色饱和度会下降儿童产品反而喜欢高饱和所以我把饱和度和不透明度分开让运营在界面上独立调整。4. 让颜色显得“真”双面渲染、笔迹混合与光照偏色处理配方和代码都能跑后紧接着的问题是画出来的颜色在手机上一看就假。这是 AR 涂色环节最玄学的一步根源不在画笔代码而在渲染设置。4.1 给模型开双面渲染Cull Off 为什么必须做涂色模型在纸上放置用户可以转着看纸背后的那面模型也会露出来。如果模型是单面渲染背面一转到摄像头方向就直接消失或变透明。默认 Standard Shader 是单面所以要先打开双面渲染。实际做法是给涂色模型单独做一个 Shader核心就加一行 CullShader Custom/ColoringUnlit { Properties { _MainTex (Paint Texture, 2D) white {} } SubShader { Tags { RenderTypeOpaque QueueGeometry } // 关键关闭背面剔除让模型正反面都显示 Cull Off Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc sampler2D _MainTex; float4 _MainTex_ST; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float3 normal : NORMAL; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; float3 normal : TEXCOORD1; }; v2f vert(appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.uv TRANSFORM_TEX(v.uv, _MainTex); o.normal UnityObjectToWorldDir(v.normal); return o; } fixed4 frag(v2f i) : SV_Target { fixed4 col tex2D(_MainTex, i.uv); // 加一点固定方向明暗避免颜色完全死板 float d saturate(dot(normalize(i.normal), normalize(float3(0.4, 0.8, 0.5)))); d d * 0.4 0.6; return fixed4(col.rgb * d, col.a); } ENDCG } } }这个 Shader 做了两件事Cull Off 保证背面可见固定方向光让模型有一点立体感又不受场景真实灯光影响。涂色产品最怕模型颜色随手机转向忽明忽暗固定方向光能稳定观感颜色跳变问题少一半。4.2 笔刷叠加模式与透明边缘的重叠问题如果不处理边缘当用户把一个区域反复涂很多遍颜色会堆积得很脏。笔刷半透明再加上 Lerp叠十次后会趋近 brushColor 而不是越来越浓观感上是“怎么涂都不均匀”。常见做法是在涂色纹理之上额外维护一张遮罩纹理只记录“涂过没涂过”叠加时根据遮罩决定是否混合。这等于给颜色区域做了选区孩子涂出界也可以一键擦除。像素级实现里我一般这样处理触摸点对应到遮罩图如果遮罩值已经是 1 就只做轻微加深否则按正常不透明度叠加。这样做的额外好处是可以实现“吸色”或“自动填充区域”。遮罩把线稿围起来的区域标记好后点一下区域内部就可以用一个封闭轮廓算法把整个区域填满输出给需要省时的用户。这一步我放在完整版里做但遮罩纹理从一开始就预留否则后期改数据结构会折腾一天。4.3 环境光和阴影两个会毁掉涂色的渲染设置AR 场景中真实环境光变化剧烈手机转个角度模型亮面变了用户会质疑“颜色变了”。两个问题最值得排查。第一材质如果是 Standard/PBR环境光探头会用真实世界的反射模型像涂了层油光。这时把 Smoothness 降到 0Metallic 设为 0再把环境光反射强度调低。如果用的是 URP可以直接给涂色材质勾选 Surface Type 为 Opaque不接额外反射源。第二模型在地面上的投影。AR 中模型投到桌面上的阴影可以有效增强空间感但阴影颜色会盖住纸张的线稿看起来像一块脏斑。我的处理是把阴影强度控制在 0.3 以下或者干脆把模型的 ShadowCaster 关掉用一条假接触阴影代替。接触阴影用 Mesh 地面加一个半透明材质就能做视觉稳定且不抖动。5. 避坑识别失败、笔迹抖动、颜色丢失和发热掉帧这章是踩坑记录每一条都是实际项目里出现过的现象和解决路径。做 AR 涂色功能上线前最少要把这四个问题都过一遍。5.1 涂色纸识别失败参考图的尺寸、对比度和打印方式现象是手机对准打印好的涂色纸模型半天不出来或者偶尔出来又消失。原因是参考图不满足 SDK 的识别条件。打印出来的涂色纸宽度最好在 15 厘米以上线稿和背景的对比度要足够强。很多涂色线稿是灰色细线相机稍微一晃就提取不到特征点。另一个常见原因是参考图纹理是纯白背景加重复花纹全局特征太少SDK 无法稳定定位。解决方法是先检查参考图像质量线条改成深黑色背景保持纯净白色宽度低于 10 厘米的纸张直接不用。打印时不要选择覆膜纸反光会让纹理在高光下丢失细节。还有一个很实用的技巧在涂色纸四角加四个实心黑圆点特征点多了识别成功率能明显提高。5.2 手挡住涂色纸导致跟踪丢失坐标回跳的兜底现象是用户涂色时手搭在纸上模型瞬间跳走或消失手移开后模型又弹回来位置和之前不一致。原因是图像跟踪在目标被遮挡时进入 limited 状态ARTrackedImage 的 transform 保持上次值但恢复跟踪时位置会有几厘米误差视觉上就是瞬跳。我的解决分两步。第一步是遮挡超时延迟销毁模型不立即跟随跟踪状态消失给用户一个宽容期。第二步是加平滑移位恢复跟踪后模型朝目标位置用插值逼近而不是硬贴。// 在 Update 里对模型做位置平滑 if (trackedImage.trackingState TrackingState.Tracking) { missingFrames 0; modelRoot.position Vector3.Lerp( modelRoot.position, trackedImage.transform.position, Time.deltaTime * 5f ); } else { missingFrames; if (missingFrames 30) modelRoot.gameObject.SetActive(false); }这里的插值系数 5f 是个调出来的经验值太大还是跳太小明显拖尾。用手在真机上反复遮挡测试感觉不到“飘”就固定下来。5.3 颜色写完后保存又丢失纹理格式、mipmap 与写入时机现象是涂完一幅作品生成分享图后发现颜色整体偏色或者重新打开 App 颜色全灰。原因是纹理格式和导入设置不一致。运行时创建的 Texture2D 默认 RGBA32但保存成 PNG 再读回来时如果 Unity 导入设置里 Alpha Is Transparency 没勾透明区域会变成黑色或灰色。另一个坑是 mipmap如果涂色纹理开了 mipmap保存时从 mip 0 读是没问题的但远处显示模糊会让用户以为颜色丢了。正确的保存流程是勾选texture.isReadable true导出前先 Apply再 EncodeToPNG_paintTexture.Apply(false); byte[] png _paintTexture.EncodeToPNG(); string path Application.persistentDataPath /coloring_ System.DateTime.Now.Ticks .png; System.IO.File.WriteAllBytes(path, png);加载回来时把纹理改成 RGBA32、关掉 mipmap、勾选 sRGB/Linear 里和项目管线一致的选项。如果加载后颜色偏淡多半是 sRGB 和 Linear 不匹配做个简单换算校验即可。5.4 低端机发热掉帧三个裁剪让项目活下来现象是几年前的手机跑十分钟就发烫涂色手感越来越钝。原因集中在三处。第一是纹理分辨率1024 对大多数模型够用如果美术想更细腻至少要确认设备的 ASTC 支持情况。第二是笔刷循环体setPixel 循环在笔刷半径 30 像素时每帧要算几千次连续拖动时 GPU 压力不小优化方式是降低绘制频率比如每两帧才处理一次触摸。第三是材质数量每个模型一套材质没问题但不要每根手指单独一个材质实例。我实际做过的裁剪方案是首版纹理固定 512笔刷半径最大 32AR 模型的面数控制在 5 万以下动画只保留待机和轻微呼吸。这样中低端机也能保持 30 帧左右。美术如果嫌弃清晰度用贴图分辨率 1024 但只给模型关键区域高密度网格整体收益最高。6. 进阶把涂色结果导出成可复用的 AR 资源包涂色画完不算完产品化要能分享和复用。这里我建议把一张涂色贴图封装成一个“资源包”的概念模型对象、涂色纹理、调色板预设三者打包后续换卡通形象、换动物模型只换模型和 UV 布局绘制系统不用改。具体验证方法是对照测试同一张纹理贴在两个不同拓扑的模型上用同样的 UV 布局导出确认笔迹位置一致。如果第二个模型出现偏移多半是它的 UV 和第一套不完全匹配这时可以用 UV 映射工具把模型展到与模板一致而不是改运行时代码。导出时记得把调色板预设也序列化用户的颜色偏好、笔刷透明度、纸张尺寸这些参数都写成 JSON 存到本地。换设备登录后读一遍体验完整度立刻上来。如果要做 AR 眼镜或 Pico 这类轻量端导出纹理直接复用Shader 换成对应平台的单面或双面版本即可。第一视角体验 AR 涂色和 VR 差别很大AR 里模型嵌在现实桌面上用户靠身体移动观察所以视觉稳定性比画面华丽更重要。我做这个方向时养成的习惯是每加一个参数先想“真实打印的纸会不会受影响”每换一台设备先画一道粗线看颜色有没有偏色时间久了这些玄学问题都会变成可预期的边界。导出格式一定提前定好 PNG配套 JSON后悔药留足后续接上传、分享、跨端复用都顺。希望这些实现思路对你帮到你。本文还有配套的精品资源点击获取
返回列表