简介:这是一份面向Unity开发者与XR爱好者的WebXR导出工具包,用于在熟悉的Unity编辑器中以C#开发并导出可在浏览器运行的WebXR体验,同时支持增强现实与虚拟现实沉浸式会话,将WebXR JavaScript API与Unity工作流打通,适合具备一定Unity基础、希望快速切入Web端AR/VR项目的中高级开发者。压缩包共419个文件,约41.2MB,包含16个cs脚本、3个asmdef程序集定义、3个jslib与1个jspre等WebGL桥接文件,以及大量meta、asset、mat、prefab等Unity工程资源,另有png、exr贴图、html/css/js模板与webxr、unityweb等配置,结构完整可直接导入使用。目前已有648人学习下载。借助该资源,读者可省去从零搭建WebXR导出链路的时间,直接研究C#与JavaScript交互、沉浸式会话配置及WebGL模板定制,并参考md文档与示例场景快速验证AR/VR效果,是入门与调试WebXR项目的实用参考。
1. 从 Unity 编辑器到浏览器标签页:这套 WebXR 导出方案到底解决了什么
如果你做过 VR/AR 项目,大概率遇到过这种局面:内容在 Unity 编辑器里跑得好好的,一到交付就卡住——对方不想装 APK,不想配头显,只想在浏览器里点开一个链接就能看。尤其是做 WebAR 营销页、WebVR 展厅、在线 3D 培训这类场景,客户要的就是「发个 URL 过去,手机和头显都能打开」。这时候 Unity 原生打包那套流程就有点使不上劲了。
unity-webxr-export 这个资源包,干的就是把 Unity 项目编译成 WebAssembly + JavaScript,让浏览器直接跑起来,并且通过 WebXR API 接入 VR 头显和 AR 会话。它不是一个插件市场里的黑匣子,而是一套可拆开看的导出工具链:Unity 侧负责场景构建和 C# 逻辑,导出后变成 WebGL 渲染 + WebXR 输入映射。适合谁?适合已经会 Unity 基础操作、想把手头项目快速搬到 Web 端的开发者,也适合做跨平台 XR Demo 验证的技术选型阶段。下面我按实际拆包和复现的顺序,把这条路走一遍。
2. 环境搭建与导出配置:从 Unity 工程到 WebGL 构建的完整链路
2.1 版本选型与包依赖:为什么不是随便一个 Unity 版本都能跑
这套导出方案对 Unity 版本有实际要求。常见做法是锁定在 Unity 2020.3 LTS 到 2022.3 LTS 之间,因为 WebGL 构建管线在这几个版本里对 WebAssembly 压缩和 IL2CPP 裁剪的支持比较稳。再新的版本不是不能用,但 WebXR 相关的输入映射偶尔会跟 XR Interaction Toolkit 的版本打架,属于玄学范畴,新手不建议一上来就追新。
包依赖方面,核心是三个东西:Unity 的 WebGL Build Support 模块、XR Plugin Management、以及 unity-webxr-export 自带的运行时 JS 桥接层。安装顺序有讲究——先装 WebGL 模块,再导入导出包,最后配 XR 设置。反过来做的话,XR 设置里可能找不到 WebXR 的 Provider 选项。
# 在 Unity Hub 里确认已安装对应版本的 WebGL Build Support # 路径:Unity Hub -> 安装 -> 对应版本 -> 添加模块 -> WebGL Build Support # 这一步不做,后面 Build Settings 里根本切不到 WebGL 平台上面这段不是代码,是操作路径的确认。很多翻车现场就是卡在这里:切平台时发现 WebGL 选项灰着,回头查半天才发现模块没装。
2.2 导出参数怎么设:几个影响成败的配置项
导入 unity-webxr-export 包之后,Unity 菜单里会多出一个导出配置面板。这里有几个参数必须手动调,默认值在 Web 端跑不通。
| 配置项 | 推荐值 | 不调的后果 |
|---|---|---|
| Compression Format | Brotli | 用 Gzip 体积偏大,用 Disabled 加载慢到怀疑人生 |
| Code Optimization | Size | 用 Speed 会导致 wasm 包体翻倍 |
| Strip Engine Code | 勾选 | 不勾的话包体里塞满用不到的引擎模块 |
| Managed Stripping Level | Medium | Low 裁不干净,High 容易把反射用到的类裁没 |
| Color Space | Gamma | WebGL 1.0 下 Linear 会偏色,除非强制 WebGL 2.0 |
这些参数背后是 WebAssembly 的加载逻辑:浏览器先拉 .wasm 文件,再解压、实例化,最后才跑 Unity 的运行时。包体每大 1MB,首屏等待就多一截。我一般会在 Player Settings 里把 Strip Engine Code 和 Managed Stripping Level 配合着调,先 Medium 跑一遍,看控制台有没有 MissingMethodException,有就降到 Low 再试。
// 在场景里挂一个初始化脚本,确保 WebXR 会话在用户手势后启动 using UnityEngine; using Unity.WebXR; public class WebXRBootstrap : MonoBehaviour { void Start() { // 浏览器要求 WebXR 必须由用户手势触发,不能自动进入 // 这里只做能力检测,真正的会话请求放到按钮回调里 if (WebXRManager.Instance != null) { Debug.Log("WebXR runtime detected, ready for session request."); } else { Debug.LogWarning("WebXR not available, fallback to 3D preview mode."); } } // 绑定到 UI 按钮的点击事件 public void OnEnterXRButtonClicked() { WebXRManager.Instance.RequestSession(); } }这段代码的逻辑说明:Start 里只做能力检测,不直接请求会话,因为浏览器安全策略要求 WebXR 必须由用户交互触发。OnEnterXRButtonClicked 绑定到 UI 按钮上,用户点一下才真正调 RequestSession。参数方面,WebXRManager.Instance 是导出包提供的单例,RequestSession 内部会根据当前设备能力选择 immersive-vr 还是 immersive-ar 模式。如果你不做这个区分,在手机上点按钮会直接报错,因为手机浏览器通常只支持 AR 会话。
2.3 构建与本地验证:怎么确认导出结果是活的
构建本身不复杂,Build Settings 切到 WebGL,点 Build,等 IL2CPP 编译完。但构建完不能直接双击 index.html 打开,浏览器对 file:// 协议下的 wasm 加载有跨域限制,必须起一个本地 HTTP 服务。
# 在构建输出目录下起一个本地服务,Python 自带的最省事 cd Build/WebGL python -m http.server 8080 # 然后浏览器访问 http://localhost:8080 # 如果页面白屏,先开 F12 看 Console 和 Network这里的关键是看 Network 面板里 .wasm 文件有没有 200 返回。如果看到 404,多半是构建时 Compression Format 选了 Brotli,但本地服务没配对应的 Content-Encoding 响应头。临时办法是改回 Gzip 重新构建,正式部署时在服务器上配 Brotli 支持。另一个常见现象是页面加载出来了但场景全黑,Console 里报 WebGL context lost,这通常是显卡驱动或浏览器硬件加速没开,跟导出包本身没关系。
3. WebXR 会话与输入映射:让头显和手机都能操作场景
3.1 会话生命周期:immersive-vr 和 immersive-ar 的切换逻辑
WebXR 的会话不是一锤子买卖,它有一套明确的状态机:检测能力 → 请求会话 → 会话运行 → 结束会话。unity-webxr-export 把这套状态机封装成了 C# 事件,但底层还是走浏览器的 navigator.xr 接口。理解这一点很重要,因为很多「头显里能跑、手机里黑屏」的问题,根源就是会话模式选错了。
immersive-vr 适合头显,进入后浏览器接管整个显示区域,Unity 渲染左右眼画面。immersive-ar 适合手机和支持 AR 的头显,进入后摄像头画面作为背景,Unity 渲染的虚拟物体叠加在上面。两者在代码里的区别就是请求会话时传的 mode 参数不同。
// 导出后的 JS 桥接层里,会话请求的核心逻辑大致是这样 // 这段是运行时自动生成的,列出来是为了理解底层行为 async function requestXRSession(mode) { if (!navigator.xr) { console.warn('WebXR not supported in this browser'); return null; } // 先检查当前设备是否支持目标模式 const supported = await navigator.xr.isSessionSupported(mode); if (!supported) { console.warn(`${mode} not supported, trying fallback`); return null; } // 请求会话,必须由用户手势触发 const session = await navigator.xr.requestSession(mode, { requiredFeatures: mode === 'immersive-ar' ? ['hit-test'] : [], optionalFeatures: ['local-floor', 'bounded-floor'] }); return session; }逻辑说明:isSessionSupported 先做能力探测,避免直接请求一个设备不支持的模式导致异常。requiredFeatures 里,AR 模式通常要加 hit-test,用于平面检测和放置物体;VR 模式一般不需要额外 required 特性。optionalFeatures 里的 local-floor 和 bounded-floor 是房间尺度追踪相关的,加了之后如果设备支持就能拿到地面高度和活动边界,不支持也不会报错。参数怎么改:如果你的 AR 场景不需要平面放置,可以把 hit-test 从 required 挪到 optional,这样能兼容更多设备。
3.2 控制器与手势输入:从 XR Input 到 Unity 事件
输入映射是 WebXR 导出里最容易出问题的一环。Unity 侧的 XR Interaction Toolkit 用了一套抽象的 Input Action,导出到 Web 端后,这些 Action 需要跟浏览器的 XRInputSource 对应上。常见做法是:手柄的 trigger 对应 select 事件,squeeze 对应 grip 事件,摇杆对应 thumbstick 轴。
// 在 Unity 里配置 XR 控制器的输入映射 using UnityEngine; using UnityEngine.XR.Interaction.Toolkit; public class ControllerSetup : MonoBehaviour { public ActionBasedController leftController; public ActionBasedController rightController; void Awake() { // 确保控制器引用不为空,Web 端初始化顺序和编辑器里不同 if (leftController == null || rightController == null) { Debug.LogError("Controller references missing, check Inspector assignment."); return; } // Web 端的手柄追踪延迟比原生高,把平滑值调大一点 leftController.selectActionTrigger = ActionBasedController.SelectActionTrigger.StateChange; rightController.selectActionTrigger = ActionBasedController.SelectActionTrigger.StateChange; } }逻辑说明:selectActionTrigger 设为 StateChange 表示按下和松开都触发,而不是只在按下时触发。Web 端因为事件传递有延迟,用 StateChange 能减少「按了没反应」的情况。参数方面,如果你的场景需要长按交互,可以改成 State 模式,但要注意 Web 端的帧率波动可能导致状态判断不准。另外,控制器引用在 Web 端初始化时可能比编辑器里晚一帧,所以 Awake 里做空检查是必要的,不然打包后直接 NullReferenceException。
3.3 渲染适配:WebGL 下的画质与性能平衡
WebGL 渲染跟原生渲染最大的区别是:没有直接访问 GPU 的权限,所有绘制指令都要经过浏览器转译。这意味着同样的场景,Web 端的帧率可能只有原生的一半。unity-webxr-export 默认会做一些降级处理,但关键参数还是得自己调。
| 参数 | 建议值 | 说明 |
|---|---|---|
| Render Scale | 0.8 ~ 1.0 | 低于 0.8 画面糊,高于 1.0 帧率掉 |
| MSAA | 2x 或关闭 | WebGL 下 MSAA 开销比原生大 |
| Shadow Distance | 15 ~ 25 | 再远阴影基本看不清,白白耗性能 |
| Texture Quality | Half Res | 全分辨率纹理在移动端浏览器容易爆显存 |
| Anti Aliasing | FXAA | 比 MSAA 省,效果够用 |
这些值不是拍脑袋定的,是在几个中低端安卓机上跑出来的经验值。如果你的目标设备是桌面浏览器或者 Quest 系列头显,可以适当往上调。但记住一个原则:Web 端的性能瓶颈通常在 CPU 侧的 draw call 提交,而不是 GPU 填充率。所以减少 draw call 比降分辨率更有效,常见做法是合并材质、用 GPU Instancing。
4. 避坑与排查:导出后白屏、手柄失灵、性能骤降的常见原因
4.1 白屏或加载卡在 90%:先查 wasm 的 MIME 类型
现象:页面打开后一直白屏,进度条卡在某个百分比不动,Console 里可能报Failed to load module script或WebAssembly.instantiate相关错误。
原因:服务器返回 .wasm 文件时 MIME 类型不对。浏览器要求 .wasm 的 Content-Type 必须是application/wasm,很多静态服务器默认给的是application/octet-stream,浏览器就拒绝实例化。
解决:在服务器配置里加一条 MIME 映射。Nginx 的话在 mime.types 里加application/wasm wasm;,Apache 在 httpd.conf 里加AddType application/wasm .wasm。本地测试用 Python 的 http.server 一般没问题,但如果你用 Node 的 serve 或 http-server,要确认版本够新。
4.2 手柄按键没反应:输入映射的初始化顺序问题
现象:头显里能看到场景,手柄也能追踪位置,但按 trigger 没反应,UI 按钮点不了。
原因:Unity 的 XR Interaction Toolkit 在 Web 端初始化时,Input Action 的绑定可能比 XR 会话建立晚。如果控制器脚本在 Awake 里就去读 Action 状态,读到的可能是空值。
解决:把输入相关的初始化放到会话建立之后。常见做法是监听 WebXRManager 的 sessionStarted 事件,在回调里再启用控制器脚本。另外检查 XR Interaction Manager 的场景引用有没有丢,Web 端对 Missing Reference 的报错不如编辑器里明显。
4.3 帧率骤降:Draw Call 和 GC 的双重暴击
现象:编辑器里跑 60 帧,导出后只有 20 帧,移动视角时明显卡顿。
原因:两个因素叠加。一是 WebGL 的 draw call 开销比原生高,二是 C# 侧的 GC 在 WebAssembly 里更频繁。每帧 new 一个 Vector3 或 string 拼接,在原生里可能无所谓,在 Web 端就是灾难。
解决:先用 Unity Profiler 连到 WebGL 构建上看 draw call 数量,超过 100 就要考虑合并。然后检查 Update 里有没有每帧分配堆内存的操作,比如GetComponent、Find、字符串拼接。把这些挪到 Start 里缓存起来。我一般会强制自己遵守一条:Update 里不出现任何 new 关键字。
4.4 AR 模式下摄像头画面不显示:权限和 HTTPS 缺一不可
现象:点了进入 AR 按钮,页面没报错,但背景是黑的,看不到摄像头画面。
原因:WebXR 的 AR 会话要求页面在 HTTPS 下运行,localhost 除外。另外摄像头权限必须由用户明确授予,如果之前拒绝过,浏览器会记住这个决定。
解决:部署时确保 HTTPS 证书有效。本地测试用 localhost 可以绕过 HTTPS 要求。如果权限被拒过,在浏览器设置里清除该站点的权限记录,刷新页面重新授权。还有一点:部分安卓浏览器对 WebXR AR 的支持不完整,Chrome for Android 是目前兼容性最好的,但也要确认版本在 81 以上。
4.5 构建报错 IL2CPP 相关:路径里有中文或空格
现象:Build 过程中报 IL2CPP 编译失败,错误信息里提到il2cpp.exe或Bee.exe返回非零值。
原因:Unity 的 IL2CPP 工具链对路径中的中文和空格支持不好。如果工程路径是D:\我的项目\XR Demo,大概率翻车。
解决:把工程移到纯英文无空格的路径下,比如D:\Projects\XRDemo。另外 Windows 用户名如果是中文,也可能影响,因为临时文件会写到用户目录下。这个坑很隐蔽,因为编辑器里跑得好好的,一构建就报错,容易误以为是代码问题。
5. 进阶技巧:用 JavaScript 桥接扩展 WebXR 能力
导出后的 Web 端并不是封闭的,unity-webxr-export 留了一套 JS 桥接机制,可以在浏览器侧直接调 Unity 的 C# 方法,反过来也行。这个能力在需要调用浏览器原生 API 时特别有用,比如获取地理位置、调摄像头拍照、或者接入第三方的 Web AR 库做图像识别。
// 在导出后的 index.html 里注入自定义 JS,调 Unity 场景里的方法 // 假设 Unity 场景里有个 GameObject 叫 WebBridge,挂了一个 WebBridge.cs function sendMessageToUnity(action, data) { if (typeof unityInstance === 'undefined') { console.warn('Unity instance not ready yet'); return; } // SendMessage 的参数:GameObject 名、方法名、参数 unityInstance.SendMessage('WebBridge', 'OnJSMessage', JSON.stringify({ action: action, payload: data, timestamp: Date.now() })); } // 示例:监听浏览器返回按钮,通知 Unity 做场景切换 window.addEventListener('popstate', function(event) { sendMessageToUnity('backPressed', { state: event.state }); });逻辑说明:unityInstance 是 Unity WebGL 构建后暴露的全局对象,SendMessage 是官方提供的通信接口。第一个参数是场景里挂脚本的 GameObject 名,第二个是 C# 侧的方法名,第三个是字符串参数。注意只能传字符串,复杂结构要先 JSON.stringify。C# 侧接收方法的签名必须是public void OnJSMessage(string json),参数类型不匹配会静默失败,Console 里不一定有报错。
反过来,C# 调 JS 用Application.ExternalCall或Application.ExternalEval,但这两个在较新版本里已经标记过时,推荐用jslib插件的方式。在 Unity 工程的 Plugins 目录下建一个.jslib文件,里面用mergeInto(LibraryManager.library, { ... })的格式声明函数,然后在 C# 里用[DllImport("__Internal")]引入。
// C# 侧声明外部 JS 函数 using System.Runtime.InteropServices; using UnityEngine; public class WebBridge : MonoBehaviour { // 引入 jslib 里定义的函数 [DllImport("__Internal")] private static extern void ShowBrowserAlert(string message); // 供 JS 侧调用的方法,方法名要和 SendMessage 里的一致 public void OnJSMessage(string json) { Debug.Log($"Received from JS: {json}"); // 解析 json 后做场景逻辑处理 } // 示例:在 Unity 里触发浏览器弹窗 public void TriggerBrowserAlert(string msg) { #if UNITY_WEBGL && !UNITY_EDITOR ShowBrowserAlert(msg); #else Debug.Log($"Editor mode, skip browser alert: {msg}"); #endif } }这段代码的关键在于条件编译:UNITY_WEBGL && !UNITY_EDITOR确保只在 WebGL 构建里调外部函数,在编辑器里跑的时候走 Debug.Log 分支,不然编辑器会报 DllNotFound 异常。这个模式我每次做 WebGL 导出都会用,算是一个固定套路。
还有一个进阶场景是自定义加载进度条。Unity 默认的加载界面比较简陋,可以在 index.html 里用createUnityInstance的 progress 回调自己画一个。
createUnityInstance(canvas, config, function(progress) { // progress 是 0 到 1 的浮点数 var bar = document.getElementById('loading-bar'); var text = document.getElementById('loading-text'); bar.style.width = (progress * 100).toFixed(1) + '%'; text.innerText = 'Loading ' + (progress * 100).toFixed(0) + '%'; }).then(function(unityInstance) { window.unityInstance = unityInstance; document.getElementById('loading-overlay').style.display = 'none'; });这个回调在 wasm 下载和解压阶段会持续触发,比 Unity 自带的进度条准确。注意 createUnityInstance 返回的是 Promise,要等它 resolve 之后才能拿到 unityInstance 对象,在那之前调 SendMessage 会报未定义。
从那以后我每次导出 WebXR 项目,都会强制走一遍「本地 HTTP 服务 + 真机浏览器 + 头显」的三层验证,不看到真机跑起来不算完。希望帮到你。
本文还有配套的精品资源,点击获取