xr-frame是微信小程序官方提供的高性能 3D 渲染框架。在 UniApp 中使用它,必须严格遵循"UniApp 页面 + 小程序原生组件"的混合开发模式。本文总结了从环境搭建到核心交互(精确点击、视角复位)的完整最佳实践。
1. 核心架构与目录规范
UniApp 的 .vue 文件无法直接编译 <xr-scene> 等标签,因此必须在 wxcomponents 目录下创建原生小程序组件,再由 UniApp 页面引用。
推荐目录结构
javascript
project-root/
├── pages/
│ └── 3dHouse/
│ └── pages/
│ └── index.vue // [UniApp] 容器页面:负责尺寸计算、Loading控制、组件通信
├── wxcomponents/ // [必要] 原生组件存放目录
│ └── xr-house/ // 自定义 3D 组件
│ ├── index.js // [Native] 业务逻辑:相机控制、事件监听
│ ├── index.json // [Native] 声明 renderer: "xr-frame"
│ ├── index.wxml // [Native] 场景结构:灯光、模型、相机
│ ├── index.wxss // [Native] 样式
│ └── animation.json // [Native] 动画配置
└── manifest.json
2. 关键配置:开启渲染器
在组件的 index.json 中,必须显式声明启用 xr-frame 渲染器。
javascript
// wxcomponents/xr-house/index.json
{
"component": true,
"renderer": "xr-frame",
"usingComponents": {}
}
3. 高清渲染与 DPI 适配 (关键)
UniApp 页面与原生组件之间最重要的数据传递是渲染尺寸 。直接传入 CSS 宽高会导致 3D 画面在真机上模糊,必须结合 pixelRatio 计算物理分辨率。
UniApp 页面 (index.vue):
JavaScript
javascript
// 获取屏幕信息并设置场景尺寸
onMounted(() => {
// #ifdef MP-WEIXIN
const info = uni.getWindowInfo()
const dpi = info.pixelRatio
// 逻辑宽高(CSS像素,用于布局占位)
width.value = info.windowWidth
height.value = info.windowHeight
// 渲染宽高(物理像素,决定渲染清晰度)
renderWidth.value = info.windowWidth * dpi
renderHeight.value = info.windowHeight * dpi
// #endif
})
原生组件 (index.wxml):
html
<xr-scene
width="{{renderWidth}}"
height="{{renderHeight}}"
style="width:{{width}}px;height:{{height}}px;"
render-system="alpha:true"
bind:ready="handleReady">
</xr-scene>
4. 交互核心:模型精确点击
为了实现对 GLTF 模型(如不规则家具)的精确点击,必须使用 mesh-shape 属性。这会指示 xr-frame 引擎利用模型的网格数据生成精确碰撞体。
原生组件 (index.wxml):
html
<xr-scene bind:ready="handleReady">
<xr-gltf
id="deskModel"
node-id="deskModel"
model="desk"
mesh-shape
bind:touch-shape="handleTouchModel"
/>
</xr-scene>
原生组件 (index.js):
javascript
methods: {
handleTouchModel({ detail }) {
// detail.value.target.id 即为 wxml 中定义的 id
const targetId = detail?.value?.target?.id;
console.log('点击了模型:', targetId);
// 标记用户已产生交互(用于优化视角重置逻辑)
this.setData({ cameraInteracted: true });
// 抛出事件给 UniApp 页面
this.triggerEvent('modelclick', { id: targetId });
}
}
5. 进阶功能:相机控制与视角重置
在 3D 展示场景中,用户拖拽视角后往往需要"一键复位"。这涉及到 UniApp 页面调用小程序原生组件方法 以及 xr-frame 内部节点操作。
A. 原生组件逻辑:操作相机节点
在 index.js 中,我们需要通过 getElementById 获取相机节点并修改其 transform 属性。
javascript
// wxcomponents/xr-house/index.js
methods: {
// ... 其他代码
// 重置视角方法(供外部调用)
resetCamera() {
// 1. 获取场景实例
if (!this.scene) return;
// 2. 获取相机节点 (需在 wxml 中定义 <xr-camera id="camera" .../>)
const cameraNode = this.scene.getElementById('camera') || this.scene.getNodeById('camera');
if (cameraNode) {
const transform = cameraNode.getComponent('transform');
// 3. 修改位置 (Position)
// 注意:camera-orbit-control 控制器主要依赖位置,朝向由控制器自动计算
// 这里将相机重置回初始坐标 (0, 3, 5)
if (transform && transform.position) {
transform.position.setValue(0, 3, 5);
console.log('Camera position reset successfully');
}
// 4. 重置交互状态标记
this.setData({ cameraInteracted: false });
}
}
}
B. UniApp 页面逻辑:跨框架调用
在 UniApp 中,无法直接通过 this.$refs 调用原生组件的方法。必须通过 selectComponent 获取原生组件实例。
javascript
// pages/3dHouse/pages/index.vue
import { getCurrentInstance } from 'vue'
const instance = getCurrentInstance()
const resetView = () => {
// #ifdef MP-WEIXIN
// 1. 获取小程序页面实例
// 在 Vue3/Vite 环境下,通常在 instance.proxy.$scope 或 instance.proxy.$mp.page 中
const mpInstance = instance.proxy.$scope || instance.proxy.$mp?.page
if (mpInstance) {
// 2. 选中原生组件 (id="xr-house")
const xrHouse = mpInstance.selectComponent('#xr-house')
// 3. 调用组件暴露的 resetCamera 方法
if (xrHouse && typeof xrHouse.resetCamera === 'function') {
xrHouse.resetCamera()
} else {
console.error('未找到 xr-house 组件或 resetCamera 方法')
}
}
// #endif
}
6. 避坑指南
-
域名白名单:
加载远程 GLB 模型、纹理图片,必须将相关域名(如 oss-cn-chengdu.aliyuncs.com)配置到小程序后台的 downloadFile 合法域名中。
-
Shadow DOM 样式隔离:
xr-house 是原生组件,受 Shadow DOM 保护。不要试图在 UniApp 的全局样式或页面样式中直接修改组件内部节点的样式。所有相关样式应写在 wxcomponents/xr-house/index.wxss 中。
-
内存泄漏:
3D 场景非常消耗内存。在组件 detached 生命周期中,务必清理所有手动绑定的事件监听器(如 scene.event.remove)和定时器。
-
真机差异:
开发者工具的模拟器基于 WebGL 实现,而真机(特别是 iOS)底层实现不同。光照效果、粒子系统和材质反射率在真机上可能与模拟器有较大差异,务必以真机表现为准。