尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Unity虚拟展馆开发实战:汉绣文化交互漫游系统

Unity虚拟展馆开发实战:汉绣文化交互漫游系统 1. 项目缘起与整体设计思路汉绣是湖北地区极具代表性的传统刺绣工艺以针法粗犷、色彩浓艳、构图饱满著称2008年被列入国家级非物质文化遗产名录。但现实情况是真正能到线下展馆参观汉绣的人并不多传统展陈方式又受限于物理空间一件绣品挂在墙上观众只能隔着玻璃看个大概针脚走向、丝线层次、背面走线这些真正体现工艺水平的东西根本看不到。我在接触这个课题的时候第一反应就是能不能用Unity搭一个虚拟展馆让观众自己走进去、凑近看、甚至把绣品拿到眼前翻转这个项目的核心目标很明确基于Unity引擎配合3D建模资源和C#脚本构建一个汉绣文化主题的虚拟展馆交互漫游系统。用户以第一人称或第三人称视角在展馆中自由行走靠近展品时触发交互可以查看绣品的高清细节、针法演示动画、工艺文字说明甚至通过UI面板了解汉绣的历史脉络和代表作品。整个系统要跑在PC端兼顾一定的画面质量和运行流畅度。为什么选Unity而不是Unreal或者Web端的Three.js这里有几个实际考量。Unreal的画面确实更强但学习曲线陡峭C门槛高对于文化类展馆这种交互逻辑不算特别复杂的项目来说有点杀鸡用牛刀。Three.js做网页版轻量展示可以但要做高质量的3D漫游、复杂的光影效果和流畅的碰撞检测性能和开发效率都不如Unity。Unity的C#脚本生态成熟Asset Store资源丰富角色控制器、UI系统、动画系统都是现成的开发周期可控。而且Unity支持后续发布到WebGL、Android等多平台扩展性强。从技术架构上整个系统分成四个层次。最底层是3D资源层包括展馆建筑模型、汉绣绣品模型、展柜、灯光、材质贴图等。往上是引擎层Unity负责渲染、物理模拟、动画播放、音频输出。再往上是逻辑层用C#脚本实现角色控制、交互检测、UI响应、场景切换等核心功能。最上面是表现层也就是用户直接看到和操作的界面包括漫游视角、交互提示、信息面板、小地图等。这个分层设计的好处是职责清晰。美术资源归美术管逻辑代码归程序管UI交互归设计管后期要替换某个绣品模型或者调整交互流程不会牵一发而动全身。我在实际开发中深刻体会到前期架构没搭好后期改一个功能要动十几个脚本那才是真正的噩梦。另一个关键决策是漫游方式的选择。第一人称视角沉浸感强适合让用户走进展馆的感觉但长时间漫游容易眩晕而且看不到自己的角色。第三人称视角能看到角色全貌适合展示虚拟形象但沉浸感弱一些。我最终采用的是双模式切换方案默认第一人称按V键切换到第三人称。这样既保证了沉浸感又给了用户选择权。角色控制器基于Unity的CharacterController组件配合C#脚本实现移动、跳跃、视角旋转碰撞检测用Capsule Collider避免穿墙和掉出场景。2. 核心细节解析与实操要点2.1 展馆场景搭建与3D建模规范展馆的3D模型是整个系统的基础。我采用的是模块化建模思路把展馆拆成地面、墙体、天花板、柱子、展柜、装饰物几个模块每个模块单独建模然后在Unity里拼装。这样做的好处是复用率高比如一根柱子可以复制多份摆在不同位置改一个材质所有柱子同步更新。建模工具用的是Blender和3ds Max。Blender免费开源适合做基础几何体和UV展开3ds Max在建筑建模方面效率更高特别是展馆这种规整的室内空间。导出格式统一用FBX这是Unity兼容性最好的3D格式。导出时要注意几个参数缩放因子设为1避免导入Unity后模型大小不对不导出摄像机灯光这些在Unity里重新创建勾选嵌入媒体确保贴图跟着模型走。注意FBX导出时如果模型面数过高导入Unity后会导致帧率骤降。建议单个展馆模型控制在5万面以内单个绣品模型控制在2万面以内。超过这个数就要做减面处理用Blender的Decimate修改器可以快速减面但要注意保留UV和法线信息。绣品模型的制作是难点。汉绣的针脚细节非常丰富如果全部用模型表现面数会爆炸。我的做法是主体用低模表现绣品的整体形态和褶皱细节针法用高清贴图加法线贴图来模拟。具体来说先用摄影测量或者手动建模做出绣品的大致形状然后在Substance Painter里绘制针脚纹理导出Diffuse、Normal、Roughness三张贴图。在Unity的Standard Shader里把Normal Map强度调到1.5左右Roughness控制在0.3到0.5之间这样在光照下能看到丝线的光泽变化凑近看也有针脚的凹凸感。展馆的灯光布置也很有讲究。汉绣色彩鲜艳如果灯光太冷或者太暗绣品的颜色就失真了。我用了三套灯光系统环境光用暖色调的Directional Light强度0.3模拟自然光展柜内部用Spot Light色温3200K左右强度1.5重点照亮绣品走廊和角落用Point Light补光强度0.5避免死黑。所有灯光都设为Baked模式提前烘焙光照贴图运行时不再计算实时光影帧率能提升30%以上。2.2 C#脚本架构与交互逻辑设计C#脚本是这个系统的灵魂。我把脚本分成三大类控制器类、交互类、UI类。控制器类负责角色移动和视角旋转交互类负责检测用户是否靠近展品并触发响应UI类负责信息面板的显示和隐藏。角色控制器脚本的核心逻辑是这样的在Update函数里读取Input.GetAxis(Horizontal)和Input.GetAxis(Vertical)获取键盘输入结合角色的朝向计算出移动向量然后调用CharacterController.Move()执行移动。视角旋转用Mouse X和Mouse Y的输入值分别控制角色左右旋转和摄像机上下俯仰。这里有个细节俯仰角度要限制在-60度到60度之间否则摄像机会翻转用户会晕。public class PlayerController : MonoBehaviour { public float moveSpeed 3.0f; public float mouseSensitivity 2.0f; public float gravity -9.81f; public Transform cameraTransform; private CharacterController controller; private float verticalRotation 0f; private Vector3 velocity; void Start() { controller GetComponentCharacterController(); Cursor.lockState CursorLockMode.Locked; } void Update() { // 移动 float h Input.GetAxis(Horizontal); float v Input.GetAxis(Vertical); Vector3 move transform.right * h transform.forward * v; controller.Move(move * moveSpeed * Time.deltaTime); // 视角 float mouseX Input.GetAxis(Mouse X) * mouseSensitivity; float mouseY Input.GetAxis(Mouse Y) * mouseSensitivity; transform.Rotate(0, mouseX, 0); verticalRotation - mouseY; verticalRotation Mathf.Clamp(verticalRotation, -60f, 60f); cameraTransform.localRotation Quaternion.Euler(verticalRotation, 0, 0); // 重力 if (controller.isGrounded velocity.y 0) velocity.y -2f; velocity.y gravity * Time.deltaTime; controller.Move(velocity * Time.deltaTime); } }交互检测用的是射线检测加触发器双保险。射线检测从摄像机中心发出射程3米如果击中展品且展品带有Interactable标签就在屏幕中心显示交互提示。触发器方案是在展品周围放一个Sphere Collider设为Trigger当角色进入触发器范围时也触发提示。两种方案结合既保证了指向性交互的精准又避免了用户必须精确对准展品才能交互的尴尬。public class InteractionDetector : MonoBehaviour { public float rayDistance 3.0f; public LayerMask interactableLayer; public GameObject interactPrompt; private IInteractable currentInteractable; void Update() { Ray ray new Ray(transform.position, transform.forward); RaycastHit hit; if (Physics.Raycast(ray, out hit, rayDistance, interactableLayer)) { IInteractable interactable hit.collider.GetComponentIInteractable(); if (interactable ! null) { currentInteractable interactable; interactPrompt.SetActive(true); if (Input.GetKeyDown(KeyCode.E)) { currentInteractable.OnInteract(); } return; } } currentInteractable null; interactPrompt.SetActive(false); } }UI面板用Unity的Canvas系统搭建信息面板包含绣品名称、年代、针法介绍、高清图片轮播。面板的显示和隐藏用CanvasGroup的alpha值控制配合DOTween插件做淡入淡出动画比直接SetActive更平滑。这里有个坑如果面板上的文字太多一次性渲染会卡顿我的做法是把文字分页每页不超过200字用户按空格键翻页。2.3 性能优化与渲染管线选择Unity的渲染管线有三套Built-in、URP、HDRP。这个项目我选的是URPUniversal Render Pipeline。原因很简单Built-in管线太老很多新特性不支持HDRP画质最好但对硬件要求太高中低端显卡跑不动URP在画质和性能之间取得了平衡支持PBR材质、实时光影、后处理效果而且对移动端和WebGL的兼容性也好。URP的配置有几个关键点。首先是在Project Settings里把Graphics和Quality的渲染管线都设为URP Asset。然后在URP Asset里调整参数Shadow Distance设为50米太远会浪费性能Shadow Cascades设为2级平衡质量和开销MSAA设为4x抗锯齿效果明显但性能开销可接受HDR开启让绣品的色彩过渡更自然。提示URP下如果发现模型阴影有锯齿或者闪烁先检查Shadow Resolution是否设为Medium以上再检查模型是否有重叠面。重叠面会导致阴影自遮挡产生难看的黑斑解决办法是把重叠面稍微偏移0.01个单位。性能优化方面我做了这几件事。第一所有静态物体勾选Static让Unity自动做批处理Draw Call从原来的200多降到60左右。第二绣品模型用LOD Group距离远时自动切换到低模近距离才显示高模。第三贴图统一压缩为ASTC格式尺寸不超过2048x2048显存占用降低了40%。第四用Occlusion Culling烘焙遮挡剔除数据被墙壁挡住的物体不渲染帧率从45提升到稳定60。还有一个容易被忽视的点Unity的GC垃圾回收会导致帧率波动。我在代码里尽量避免在Update里new对象字符串拼接用StringBuilder列表用对象池复用。特别是交互提示的显示隐藏不要每次都Instantiate和Destroy而是提前创建好用SetActive控制。3. 实操过程与核心环节实现3.1 从零搭建Unity项目环境第一步是安装Unity Hub和Unity Editor。版本选择上我推荐Unity 2022 LTS长期支持版这个版本稳定URP功能完善社区资源多。安装时勾选Windows Build Support和WebGL Build Support方便后续发布。安装完成后在Unity Hub里新建项目模板选3D (URP)项目名设为HanXiuVirtualMuseum。项目创建后先做基础配置。在Edit Project Settings里把Company Name和Product Name填好Color Space设为Linear这样光照计算更准确。在Quality设置里把各平台的Quality Level调成一致避免发布后画质突变。在Input Manager里检查Horizontal、Vertical、Mouse X、Mouse Y这几个轴是否正常后面角色控制要用。然后导入必要的Package。在Window Package Manager里安装Cinemachine摄像机控制、TextMeshPro高质量文字、Post Processing后处理效果。Cinemachine用来做第三人称摄像机的跟随和碰撞回避比手写脚本稳定得多。TextMeshPro的中文字体需要额外制作用Font Asset Creator生成字符集选常用汉字加标点图集分辨率设4096x4096否则文字会模糊。3.2 展馆场景的搭建与光照烘焙场景搭建从地面开始。在Hierarchy里右键创建3D Object Plane缩放设为10x10这就是展馆的地面。然后创建Cube作为墙体调整位置和缩放围成一个矩形空间。展馆的尺寸我设为长30米、宽20米、高5米这个尺度下漫游不会觉得压抑也不会因为太大而显得空旷。展柜用Cube加透明材质制作。底座是不透明的深色木纹材质上半部分是透明玻璃材质。玻璃材质在URP里用Lit ShaderSurface Type设为TransparentAlpha设为0.3Smoothness调到0.95这样看起来有玻璃的通透感和反光。展柜内部放一个Spot Light专门照亮绣品灯光角度设为60度强度2.0范围3米。绣品模型的导入要注意坐标。从Blender导出的FBX在Unity里导入后Position设为(0, 1.5, 0)Rotation设为(0, 0, 0)Scale保持1。如果模型朝向不对在Blender里调整好再导出不要在Unity里旋转否则后续做动画会出问题。绣品放在展柜里位置要居中距离玻璃面至少10厘米避免穿模。光照烘焙是场景搭建中最耗时的环节。在Window Rendering Lighting里把Lighting Settings打开。Environment Lighting的Source设为SkyboxIntensity Multiplier设为1.0。Realtime Global Illumination关掉用Baked GI。Lightmapper选Progressive CPU虽然比GPU慢但质量更稳定。Lightmap Resolution设为40 texels per unit这个值下烘焙出来的光影比较细腻。Lightmap Padding设为2避免不同物体的光照贴图互相干扰。点击Generate Lighting后烘焙时间取决于场景复杂度。我这个场景大概烘了15分钟。烘焙完成后检查有没有漏光或者黑斑。漏光通常是墙体有缝隙用ProBuilder补面或者调整模型位置。黑斑一般是模型法线反了在Blender里Recalculate Normals再导出。3.3 交互功能的完整实现流程交互功能的核心是IInteractable接口。所有可交互的展品都实现这个接口接口里定义OnInteract()方法。绣品展品继承MonoBehaviour并实现IInteractable在OnInteract()里调用UIManager显示信息面板。public interface IInteractable { void OnInteract(); string GetInteractionPrompt(); } public class EmbroideryExhibit : MonoBehaviour, IInteractable { public string exhibitName; public string era; public string stitchType; public string description; public Sprite[] detailImages; public void OnInteract() { UIManager.Instance.ShowExhibitPanel(this); } public string GetInteractionPrompt() { return 按 E 查看 exhibitName; } }UIManager用单例模式保证全局只有一个实例。ShowExhibitPanel方法接收EmbroideryExhibit对象把里面的数据填充到UI面板上。面板上的图片轮播用协程实现每隔3秒切换一张用户也可以点击左右箭头手动切换。public class UIManager : MonoBehaviour { public static UIManager Instance; public GameObject exhibitPanel; public TextMeshProUGUI nameText; public TextMeshProUGUI eraText; public TextMeshProUGUI stitchText; public TextMeshProUGUI descText; public Image detailImage; private EmbroideryExhibit currentExhibit; private int currentImageIndex 0; void Awake() { if (Instance null) Instance this; else Destroy(gameObject); } public void ShowExhibitPanel(EmbroideryExhibit exhibit) { currentExhibit exhibit; currentImageIndex 0; nameText.text exhibit.exhibitName; eraText.text 年代 exhibit.era; stitchText.text 针法 exhibit.stitchType; descText.text exhibit.description; if (exhibit.detailImages.Length 0) detailImage.sprite exhibit.detailImages[0]; exhibitPanel.SetActive(true); Cursor.lockState CursorLockMode.None; Cursor.visible true; } public void CloseExhibitPanel() { exhibitPanel.SetActive(false); Cursor.lockState CursorLockMode.Locked; Cursor.visible false; } }针法演示动画用Unity的Animator实现。以平针绣为例做一个针线穿过布料的动画针的模型沿路径移动线用Line Renderer跟随。动画时长3秒循环播放。在Animator里创建状态机Idle状态和Playing状态之间用Trigger切换。用户点击播放演示按钮时触发Trigger动画播放一次后自动回到Idle。3.4 漫游系统的移动与碰撞处理移动系统前面已经给了核心代码这里补充几个实操细节。CharacterController的Slope Limit设为45度Step Offset设为0.3米这样角色能上楼梯但不能爬陡坡。Skin Width设为0.08太小会导致角色卡在墙角太大则会出现悬浮。碰撞检测用Layer来做过滤。把展馆建筑设为Default层展品设为Interactable层角色设为Player层。在Physics设置里取消Player和Interactable的碰撞这样角色可以穿过展品但不会触发物理碰撞交互检测用射线单独处理。Player和Default保持碰撞角色不会穿墙。注意如果发现角色能穿过展柜检查展柜的Collider是否被误删。有时候为了优化性能会把Collider去掉但展柜这种需要阻挡角色的物体必须保留Collider。可以用Box Collider近似比Mesh Collider性能好。小地图功能用第二个摄像机实现。在角色头顶上方20米处放一个摄像机朝向正下方Orthographic Size设为15渲染层只勾选Minimap层。把渲染结果输出到Render Texture在UI的RawImage上显示。小地图上用一个三角形图标表示角色位置和朝向图标随角色旋转。4. 常见问题与排查技巧实录4.1 渲染与画面类问题速查问题现象可能原因排查与解决绣品颜色发灰Color Space设为GammaProject Settings Player Color Space改为Linear模型阴影闪烁Shadow Bias过小在光源设置里把Bias调到0.05Normal Bias调到0.4玻璃展柜不透明材质Surface Type没改选中材质Surface Type改为TransparentAlpha调低画面整体偏暗环境光强度不够Lighting设置里Environment Intensity调到1.5远处物体模糊Mip Map过度贴图设置里把Mip Map Bias调到-0.5帧率突然下降GC频繁触发用Profiler查看GC Alloc优化Update里的new操作阴影问题是我踩过最大的坑。一开始展馆里的阴影全是锯齿调了Shadow Resolution也没用。后来发现是URP Asset里的Shadow Cascades设成了1级远处阴影精度不够。改成2级后明显改善。还有一个问题是绣品在展柜里会有自阴影看起来脏脏的。解决办法是在绣品的材质上勾选Receive Shadows但取消Cast Shadows这样绣品不产生阴影但能接收展柜的阴影。4.2 交互与逻辑类问题排查交互失灵是最常见的问题。用户反馈按E没反应排查思路是这样的先看射线有没有击中物体在Scene视图里画一条Debug.DrawRay确认射线方向正确。再看物体的Layer是否在interactableLayer里Layer不对射线检测直接过滤掉了。最后看物体有没有Collider没有Collider射线检测不到。还有一种情况是交互提示显示了但按E没反应。这通常是IInteractable接口没实现或者OnInteract()方法里报错了。在Console里看有没有NullReferenceException如果有检查UIManager.Instance是否为空场景里有没有UIManager对象。UI面板打开后角色还在移动这也是个常见问题。原因是打开面板时没有锁定角色控制。解决办法是在ShowExhibitPanel里把PlayerController的enabled设为false关闭面板时再设回true。同时把Cursor的锁定状态改回来否则鼠标动不了。4.3 性能优化实战经验Draw Call过高是性能杀手。我一开始的场景Draw Call有200多帧率只有30。优化步骤第一步把所有静态物体勾选StaticUnity自动做Static BatchingDraw Call降到120。第二步把相同材质的物体合并比如展馆的柱子用同一个材质Draw Call降到80。第三步用GPU Instancing把重复的展柜、装饰物开启InstancingDraw Call降到60。第四步用Occlusion Culling被遮挡的物体不渲染Draw Call稳定在40左右。内存占用也要关注。Unity的Profiler里看Texture Memory和Mesh Memory。贴图压缩格式选ASTC 6x6比RGBA32省一半内存。模型导入时勾选Mesh Compression设为Medium能减少30%的网格内存。音频文件用Vorbis格式质量设70%比PCM省80%空间。提示如果发布到WebGL平台内存限制更严格。建议把贴图尺寸降到1024x1024模型面数再砍一半音频用单声道。WebGL的初始加载时间也要控制把资源打成AssetBundle按需加载首屏加载控制在10秒以内。4.4 发布与部署注意事项PC端发布比较简单File Build Settings里选Windows x86_64Player Settings里设好分辨率1920x1080、全屏模式、图标。Build出来的exe文件加上Data文件夹一起打包用户双击exe就能运行。WebGL发布要注意几个点。在Player Settings里把Compression Format设为Brotli比Gzip压缩率高。把Strip Engine Code勾上去掉没用到的引擎模块包体能小20%。把Managed Stripping Level设为High但要注意有些反射调用的代码会被误删需要加link.xml保护。发布到WebGL后如果发现加载卡在90%通常是内存不够。在Player Settings里把Memory Size调大或者优化资源。如果发现运行卡顿检查是不是用了System.ThreadingWebGL不支持多线程所有代码都在主线程跑耗时的操作要拆成协程分帧执行。5. 项目扩展与个人实操体会这个系统做完基础版本后其实还有很多可以扩展的方向。比如加入语音导览功能用户点击展品后播放对应的语音介绍用Unity的AudioSource加AudioClip就能实现。再比如加入多语言支持用Unity的Localization Package把中文、英文、日文的文本做成不同的Locale用户可以在设置里切换。还有一个有意思的扩展是虚拟绣娘角色。用Unity的Animator做一个简单的角色动画绣娘坐在绣架前手部做刺绣动作。用户靠近时绣娘抬头微笑播放欢迎语音。这个功能用Timeline做动画序列配合Cinemachine的镜头切换效果会很生动。不过角色建模和动画制作成本较高适合有美术资源的团队。我在这个项目里最大的体会是虚拟展馆的核心不是技术多炫酷而是内容是否准确、交互是否自然。汉绣的针法有几十种每种针法的走向、叠压关系、丝线光泽都不一样。如果模型和贴图做得不对懂行的人一眼就能看出来。所以前期一定要找汉绣传承人做顾问把每种针法的细节搞清楚再动手建模。另一个体会是性能优化要贯穿始终不要等到最后才做。我一开始为了赶进度模型面数没控制贴图尺寸没压缩结果后期优化花了整整一周。如果一开始就按规范来能省很多时间。建议在项目启动时就定好资源规范模型面数上限、贴图尺寸上限、材质数量上限美术和程序都按这个标准执行。最后分享一个交互设计的小技巧交互提示不要只显示文字加一个图标或者高亮效果会更直观。我在展品周围加了一个缓慢旋转的光环用户靠近时光环变亮配合按E查看的文字提示交互意图非常明确。这个光环用Shader Graph做一个简单的菲涅尔效果成本很低但体验提升明显。
返回列表