玩转小程序AR-实战篇

《玩转小程序AR》系列教程

声明: 本文所载内容仅限于学习交流之目的。所有抓包内容、敏感网址及数据接口均已进行脱敏处理,严禁将其用于商业或非法用途。任何因此产生的后果,作者不承担任何责任。若涉及侵权,请及时联系作者以便立即删除。

逆向小程序

接着前文: 《玩转小程序AR-基础篇》,体验过原神官方AR小程序后,我也比较好奇他们实现AR Live2d 动画的原理。

出于技术学习 的目的,在开源社区搜寻微信小程序反编译工具,发现 KillWxapkgunveilr 暂时仍可使用

逆向 某神AR 小程序

首先我们需要找到某神AR微信小程序的本地小程序包地址

注意:本人电脑 Mac (Windows电脑同理)

  1. 进入电脑微信小程序目录
bash 复制代码
# Mac 目录地址
cd /Users/你的电脑用户名/Library/Containers/com.tencent.xinWeChat/Data/Documents/app_data/radium/Applet/packages

# Windows 目录地址
cd C:\Users\你的电脑用户名\AppData\Roaming\Tencent\xwechat\radium\Applet\packages


# 打开文件夹
open .

如果发现目录下有较多文件夹,建议先把所有的文件夹都删除,为后续定位要逆向的小程序做好准备

  1. 用电脑微信打开需要逆向的小程序
  1. 再次打开微信小程序目录

这时候,恭喜你已经定位到了小程序的AppID了!

  1. 反编译小程序

小程序文件夹下的__APP__.wxapkg就是编译后的小程序,现在我们需要使用工具反编译它了

  • unveilr 工具

安装地址

bash 复制代码
# 安装反编译工具
npm i unveilr -g

# 运行反编译命令
unveilr wx -i wxb2618d769d6f5143  "/Users/你的电脑用户名/Library/Containers/com.tencent.xinWeChat/Data/Documents/app_data/radium/Applet/packages/wxb2618d769d6f5143/3/__APP__.wxapkg" -f
  • KillWxapkg 工具

安装地址

bash 复制代码
./KillWxapkg -id="wxb2618d769d6f5143" -in="/Users/你的电脑用户名/Library/Containers/com.tencent.xinWeChat/Data/Documents/app_data/radium/Applet/packages/wxb2618d769d6f5143/3"  -restore

于是在小程序文件夹下,就新生成了一个反编译后的源码文件夹

当然,反编译后的源码也不是100%还原:

  • 缺失 wxml
  • js 代码被babel转码后,语义不是特别清晰

当然,在AI的加持下,如今这些问题已经完全难不倒我们了。AI分分钟就能根据混淆过的js原始逻辑,还原清晰可读的语义化代码

从源码中可以看到,原神AR小程序使用的正是XR-FRAME框架

逆向 某cube 小程序插件

官方提供的kivicube插件

json 复制代码
{
  "usingComponents": {
    "kivicube-scene": "plugin://kivicube/kivicube-scene"
  },
  "disableScroll": true,
  "navigationStyle": "custom"
}
xml 复制代码
<kivicube-scene
  wx:if="{{showAR}}"
  class="kivicube"
  scene-id="{{sceneId}}"
  bind:ready="ready"
  bind:error="error"
  bind:downloadAssetStart="downloadStart"
  bind:downloadAssetProgress="downloadProgress"
  bind:downloadAssetEnd="downloadEnd"
  bind:loadSceneStart="loadStart"
  bind:loadSceneEnd="loadEnd"
  bind:sceneStart="sceneStart"
  bind:openUrl="openUrl"
  bind:photo="photo"
/>

使用同样的方法,我们可以得到小程序插件逆向的源码

通过源码分析可得知,Kivicube 使用的是底层VisionKit + 自研的Threejs封装

着色器

Shader,中文称为"着色器",是一种在图形处理单元(GPU)上运行的计算机程序,用于定义和控制图形渲染过程中的各种视觉效果
使用 GLSL 的着色器(shader),GLSL 是一门特殊的有着类似于 C 语言的语法,在图形管道 (graphic pipeline) 中直接可执行的 OpenGL 着色语言。

更多详情见MDN上的解释:GLSL Shaders

XR-FRME 提供了自定义效果的能力: 定制一个效果

序列帧 SHADER

XR-FRAME 官方的《序列帧动画(雪碧图、GIF)》示例,实现了一个简单的可配置的序列帧效果

而原神小程序AR的源码中,正是使用了这个序列帧效果实现了伪Live2d

首先我们需要将序列帧动画合成到一张M行*N列 大小的PNG图片上(注意:微信小程序最大能渲染8000x8000左右分辨率的序列帧图片)

如上图所示,我们这次的图片是8行x4列,共32张序列帧图片合成而来

源码示例: 序列帧 SHADER

xml 复制代码
<xr-scene bind:ready="handleReady">
  <xr-assets></xr-assets>
  <xr-node>
    <xr-node node-id="center" />
    <xr-mesh visible="{{meshesVisible}}" id="animation-mesh" node-id="animation-mesh" position="0 0 0" scale="1 1 1.3" rotation="90 0 0" geometry="plane" />
  </xr-node>
  <xr-camera target="center" clear-color="0.4 0.8 0.6 1" position="0 0 2.5" camera-orbit-control />
</xr-scene>
javascript 复制代码
Component({
  /**
   * 组件的初始数据
   */
  data: {
    meshesVisible: false
  },

  /**
   * 组件的方法列表
   */
  methods: {
    handleReady: function ({ detail }) {
      const xrFrameSystem = wx.getXrFrameSystem()
      const createFrameEffect = (scene) => {
        return scene.createEffect({
          name: 'frame-effect',
          properties: [
            {
              key: 'columCount', // 列数
              type: xrFrameSystem.EUniformType.FLOAT,
              default: 1
            },
            {
              key: 'rowCount', // 行数
              type: xrFrameSystem.EUniformType.FLOAT,
              default: 1
            },
            {
              key: 'during', // 持续时间
              type: xrFrameSystem.EUniformType.FLOAT,
              default: 1
            }
          ],
          images: [
            {
              key: 'u_baseColorMap',
              default: 'white',
              macro: 'WX_USE_BASECOLORMAP'
            }
          ],
          // 透明物体需要大于`2500`!
          defaultRenderQueue: 2501,
          passes: [
            {
              renderStates: {
                blendOn: false,
                depthWrite: true,
                cullOn: false,
                // 基础库 v3.0.1 开始 默认的 plane 切为适配 cw 的顶点绕序
              },
              lightMode: 'ForwardBase',
              useMaterialRenderStates: true,
              shaders: [0, 1]
            }
          ],
          shaders: [
            // 顶点着色器 Vertex shaders
            `#version 100

          precision highp float;
          precision highp int;
    
          attribute vec3 a_position;
          attribute highp vec2 a_texCoord;
      
          uniform mat4 u_view;
          uniform mat4 u_projection;
          uniform mat4 u_world;
          varying highp vec2 v_uv;
          void main()
          {
            v_uv = a_texCoord;
            gl_Position = u_projection * u_view * u_world * vec4(a_position, 1.0);
          }`,
            // 片段着色器 Fragment shaders
            `#version 100
            precision highp float;
            precision highp int;

            uniform sampler2D u_baseColorMap;
            uniform highp float u_gameTime;
            uniform highp float rowCount;
            uniform highp float columCount;
            uniform highp float during;
            varying highp vec2 v_uv;
            void main()
            {
              float loopTime = mod(u_gameTime, during);

              float tickPerFrame = during / (columCount * rowCount);
              
              float columTick = mod(floor(loopTime / tickPerFrame), columCount);
              float rowTick = floor(loopTime / tickPerFrame / columCount);

              vec2 texCoord = vec2(v_uv.x / columCount + (1.0 / columCount) * columTick , v_uv.y / rowCount + (1.0 / rowCount) * rowTick);
              vec4 color = texture2D(u_baseColorMap, texCoord);
              gl_FragColor = color;
            }`
          ],
        });
      }
      xrFrameSystem.registerEffect('frame-effect', createFrameEffect)
      this.scene = detail.value

      this.loadAsset()
    },

    async loadAsset() {
      const xrFrameSystem = wx.getXrFrameSystem();
      const xrScene = this.scene;

      await xrScene.assets.loadAsset({
        type: 'texture',
        assetId: 'lzy',
        src: 'https://assets.xxxx.com/resources/cdn/20251022/0ac5e7c80c0fc262.png',
      })

      // 第一个参数是效果实例的引用,第二个参数是默认`uniforms`
      const frameMaterial = xrScene.createMaterial(
        // 使用定制的效果
        xrScene.assets.getAsset('effect', 'frame-effect'),
        { u_baseColorMap: xrScene.assets.getAsset('texture', 'lzy') }
      )

      // 可以将其添加到资源系统中备用
      xrScene.assets.addAsset('material', 'frame-effect', frameMaterial)

      const meshElement = xrScene.getElementById('animation-mesh').getComponent(xrFrameSystem.Mesh)
      frameMaterial.setFloat('columCount', 4)
      frameMaterial.setFloat('rowCount', 8)
      frameMaterial.setFloat('during', 1)
      frameMaterial.alphaMode = "BLEND"
      meshElement.material = frameMaterial

      this.setData({
        meshesVisible: true
      })
    },
  }
})

透明视频 SHADER

一般的透明视频:

  1. 自带透明通道的视频格式: mov (小程序默认不支持mov格式播放)
  2. 特殊处理后的左右分屏视频格式: mp4 (小程序默认支持mp4格式播放)
  • 左边是视频的 RGB
  • 右边是视频的 Alpha
  • 左右叠加即可渲染透明视

更多详情见前文《更高效的web动效解决方案 - 背景视频》

XR-FRAME 官方的《过滤黑色背景视频》示例,正好演示了左右分屏视频的过滤黑色背景能力

源码示例: 透明视频 SHADER

xml 复制代码
<xr-scene bind:ready="handleReady">
  <xr-assets bind:progress="handleAssetsProgress" bind:loaded="handleAssetsLoaded">
    <xr-asset-load type="video-texture" asset-id="lzy" src="https://assets.xxxx.com/resources/cdn/20251022/bd7cb6ba6546d697.mp4" options="autoPlay:true,loop:true" />
    <xr-asset-material asset-id="removeBlack-mat" effect="removeBlack" />
  </xr-assets>
  <xr-node>
    <xr-node node-id="center" />
    <xr-node wx:if="{{loaded}}">
      <xr-mesh node-id="video-item" position="0 0 0" rotation="90 0 0" scale="1 1 1.3" geometry="plane" material="removeBlack-mat" uniforms="u_videoMap: video-lzy" />
    </xr-node>
  </xr-node>
  <xr-camera target="center" clear-color="0.4 0.8 0.6 1" position="0 0 3" camera-orbit-control />
</xr-scene>
javascript 复制代码
const xrFrameSystem = wx.getXrFrameSystem();

xrFrameSystem.registerEffect('removeBlack', scene => scene.createEffect({
  name: "removeBlack",
  images: [{
    key: 'u_videoMap',
    default: 'white',
    macro: 'WX_USE_VIDEOMAP'
  }],
  defaultRenderQueue: 2000,
  passes: [{
    "renderStates": {
      cullOn: false,
      blendOn: true,
      blendSrc: xrFrameSystem.EBlendFactor.SRC_ALPHA,
      blendDst: xrFrameSystem.EBlendFactor.ONE_MINUS_SRC_ALPHA,
      cullFace: xrFrameSystem.ECullMode.BACK,
    },
    lightMode: "ForwardBase",
    useMaterialRenderStates: true,
    shaders: [0, 1]
  }],
  shaders: [
    // 顶点着色器 Vertex shaders
    `#version 100

uniform highp mat4 u_view;
uniform highp mat4 u_viewInverse;
uniform highp mat4 u_vp;
uniform highp mat4 u_projection;
uniform highp mat4 u_world;

attribute vec3 a_position;
attribute highp vec2 a_texCoord;

varying highp vec2 v_UV;

void main()
{
  v_UV = a_texCoord;
  vec4 worldPosition = u_world * vec4(a_position, 1.0);
  gl_Position = u_projection * u_view * worldPosition;
  }`,
    // 片段着色器 Fragment shaders
    `#version 100

precision mediump float;
precision highp int;
varying highp vec2 v_UV;

#ifdef WX_USE_VIDEOMAP
  uniform sampler2D u_videoMap;
#endif

void main()
{
#ifdef WX_USE_VIDEOMAP
  // 左右分屏透明视频处理:
  // 左半边 (0-0.5) 为彩色内容,右半边 (0.5-1.0) 为透明度遮罩
  
  // 1. 采样左半边获取 RGB 颜色
  vec2 colorUV = vec2(v_UV.x * 0.5, v_UV.y);
  vec4 color = texture2D(u_videoMap, colorUV);
  
  // 2. 采样右半边获取 Alpha 遮罩
  vec2 alphaUV = vec2(v_UV.x * 0.5 + 0.5, v_UV.y);
  vec4 alphaSample = texture2D(u_videoMap, alphaUV);
  float alpha = alphaSample.r; // 使用红色通道作为透明度(灰度值)
  
  // 3. 输出颜色 + 遮罩透明度(不做伽马校正,避免变暗)
  gl_FragData[0] = vec4(color.rgb, alpha);
#else
  gl_FragData[0] = vec4(1.0, 1.0, 1.0, 1.0);
#endif
}
`],
}));

Component({
  /**
   * 组件的初始数据
   */
  data: {

  },

  /**
   * 组件的方法列表
   */
  methods: {
    handleReady({
      detail
    }) {
      console.log('handleReady', detail.value)
    },
    handleAssetsProgress({ detail }) {
      console.log('assets progress', detail.value)
    },
    handleAssetsLoaded({ detail }) {
      console.log('assets loaded', detail.value)
      this.setData({ loaded: true })
    },
  }
})

实战案例

源码示例: 序列帧 SHADER

同层渲染

目前XR-FRAME尚未支持和小程序的UI元素混写,但我们可以使用同层方案

xml 复制代码
<view>
  <demo8
    disable-scroll
    id="main-frame"
    width="{{renderWidth}}"
    height="{{renderHeight}}"
    style="width:{{width}}px;height:{{height}}px;top:{{top}}px;left:{{left}}px;"
    bind:arTrackerSwitch="handleTrackerSwitch"
    markerImg="{{markerImg}}"
  />
  <view class="marker-tip-container" hidden="{{hiddenTip}}">
    <view class="marker-img-container">
      <image mode="aspectFit" class="marker-img" src="{{markerImg}}" />
    </view>
    <view class="marker-text-container">
      <text class="marker-text">请对准识别图</text>
    </view>
  </view>
</view>

demo8是XR-FRAME组件,它和viewUI组件处在同一层,这既是所谓的同层渲染方案。而同层方案,就必然涉及到组件通信

XR-FRAME组件需要将AR的识别状态同步给父级,父级根据不同的AR状态,展示不同的UI界面。而这些通信方式,和传统的组件通信方式基本一致:父级传递函数和属性到子级,子级通过执行回调函数传递数据

框架维护

比较尴尬的是,核心技术负责人已离开团队,XR-FRAME框架处于暂停维护状态

资料

相关推荐
银空飞羽1 小时前
让Trae SOLO全自主学习开发近期爆出的React RCE漏洞靶场并自主利用验证(CVE-2025-55182)
前端·人工智能·安全
钮钴禄·爱因斯晨1 小时前
DevUI 组件生态与 MateChat 智能应用:企业级前端智能化实战
前端
不会写DN2 小时前
存储管理在开发中有哪些应用?
前端·后端
清风细雨_林木木2 小时前
Obsidian 笔试环境配置与使用指南
前端
用户47949283569153 小时前
Vite8来啦,告别 esbuild + Rollup,Vite 8 统一用 Rolldown 了
前端·javascript·vite
枫,为落叶3 小时前
VueRouter前端路由
前端
踢球的打工仔3 小时前
前端知识介绍
前端
草字3 小时前
uniapp 悬浮按钮支持可拖拽。可移动。
前端·javascript·uni-app