# 第三方H5引擎混合渲染

微信小游戏3D支持与第三方H5引擎混合渲染的能力,但无法对第三方引擎进行加速。

开发者可以2D使用其他引擎渲染,3D使用小游戏框架渲染来完成开发,可以对现有小游戏针对性的性能提升,或是可以渐进式优化,保留原本部分的开发模式。

在微信小游戏中,第三方引擎只能绘制在一个离屏canvas上,要渲染在小游戏中,只能借助小游戏的engine.Texture2D,并通过engine.UISprite组件将离屏canvas的内容绘制在小游戏UI界面中。可以将第三方引擎渲染的结果看做小游戏的一个普通的UI节点,因此可以与小游戏的其他3D和2D节点混合。

Texture2D提供了两个接口可以实现这样的功能,但都有一定的限制,另外,编辑工具提供了另一种只在工具中渲染的接口。三种接口的对比说明如下

# 三种渲染方式的对比

HudCanvas ReadPixels InitWithCanvas
接口(可参考下面的脚本) engine.Editor.API.CanvasBgManager
.setCustomHudCanvas(canvas)
texture2d.initDynamicTexture()
texture2d.updateSubTexture()
texture2d.initWithCanvas()
原理 将第三方引擎创建出来的canvas通过webgl texImage2D的方式同步渲染在编辑器的hudCanvas上。 每帧对第三方引擎创建出来的canvas进行readPixels,并用readPixels出来的arraybuffer更新texture2d的渲染内容。 通过传入第三方引擎创建出来的canvas,小游戏框架底层会同步该canvas的渲染内容到texture2d上。
适用范围 工具播放态 工具播放态、模拟器、真机 真机
性能
是否支持与小游戏2d渲染混用 不支持 支持 支持

# 注意事项:

  1. 由于第三方引擎的渲染依赖于脚本的执行,渲染结果无法在工具的编辑态,以及Scene窗口中展示出来,只能在播放态的Game窗口中渲染。
  2. 在真机上渲染,只建议使用initWithCanvas的方式。
  3. 在模拟器中展示只能使用ReadPixels的方式。
  4. 在工具播放态,若有与小游戏2d渲染混用的需求,请使用ReadPixels的方式;若没有,即2d完全由第三方引擎渲染,则使用HudCanvas的方式。

# 示例

打开小游戏版开发者工具,然后点击下面的新手引导链接即可直接下载引导并开始学习。

下面的示例代码,是第三方引擎在小游戏中启动的脚本组件,应挂载在一个空2d场景的节点上,此脚本会创建一个2D子节点和UISprite组件以及Texture2D承接第三方引擎的渲染结果,并配置在不同情况使用的不同接口。

import engine from "engine";
declare var require: any

@engine.decorators.serialize("ThirdEngineStartUp")
export default class ThirdEngineStartUp extends engine.Script {
  private _thirdEngineNode?: engine.Entity; // 用于承接第三方引擎渲染内容的节点
  private _uiSprite?: engine.UISprite // 用于承接第三方引擎渲染内容的渲染组件
  private _tex?: engine.Texture2D; // 用于承接第三方引擎渲染内容的texture2d
  private _pixels?: Uint8Array; // 从第三方引擎中读出的渲染像素值。 

  private _systemInfo = wx.getSystemInfoSync() // 获取系统信息
  private _useReadPixelsMethodInEditor = false // 在工具上绘制是否使用readPixels的方式,readPixels的方式表现真实,但是在工具上绘制效率低。
  private _inEditor = (engine as any).Editor && (engine as any).Editor.API && (engine as any).Editor.API.CanvasBgManager // 是否在小游戏工具中播放
  private _inSimulator = this._systemInfo.platform === "devtools" // 是否在模拟器中播放

  private _hasSetHudCanvas = false // 是否已经设置过hudCanvas

  public onAwake() {
    // 把第三方引擎项目构建出来的微信小游戏game.js入口脚本require进来。
    // require('/game.js')
  
    // 创建一个子节点用于承接第三方引擎的渲染结果
    this._createNodeWithUISprite()
  }
  public onUpdate(dt) {
    if (/** 第三方引擎初始化完成 */) {
      this._render()
    }
  }
  public onDestroy() {
    this.clear()
  }

  // 创建一个有UISprite组件的2d节点,UISprite组件上的spriteframe(所对应的texture2d)用于代理显示第三方引擎渲染的结果
  private _createNodeWithUISprite() {
    const SHAREDWIDTH = engine.game.rootUICanvas.entity.transform2D.size.x;
    const SHAREDHEIGHT = engine.game.rootUICanvas.entity.transform2D.size.y;

    if (!this._thirdEngineNode) {
      this._thirdEngineNode = engine.game.createEntity2D("thirdEngineNode");
      this._thirdEngineNode.transform2D.size.x = SHAREDWIDTH;
      this._thirdEngineNode.transform2D.size.y = SHAREDHEIGHT;
      this._thirdEngineNode.transform2D.position.x = 0;
      this._thirdEngineNode.transform2D.position.y = 0;
    
      this.entity.transform2D.addChild(this._thirdEngineNode.transform2D);

      this._uiSprite = this._thirdEngineNode.addComponent(engine.UISprite);
      this._uiSprite.colorBlendType = engine.BlendType.Alpha;

      // 由于框架里面的坐标系与HTMLCanvas的坐标系垂直方向相反,所以需要对绘制的RenderTexture做一个垂直方向的翻转
      this._thirdEngineNode.transform2D.scaleY = -1;
    }
  }

  /**
   * 有三种方式,可以将第三方引擎绘制结果,渲染出来:
   * 1. renderByReadPixels的方式,原理:每帧对第三方引擎创建出来的canvas进行readPixels,并用readPixels出来的arraybuffer更新texture2d的渲染内容。
   *    此方法是小游戏工具、真机、模拟器都通用的渲染方式。因为要每帧readPixels,所以性能很低。
   * 2. renderInRuntimeByInitWithCanvas,原理:通过传入第三方引擎创建出来的canvas,小游戏框架底层会同步该canvas的渲染内容到texture2d上。
   *    此方法只能在真机上正常渲染,小游戏工具和模拟器中都无法正常显示。性能好。
   * 3. renderInEditorByHudCanvas的方式,原理:将第三方引擎创建出来的canvas通过webgl texImage2D的方式同步渲染在编辑器的hudCanvas上。
   *    此方法只能值小游戏工具内使用,无法在模拟器和真机上使用。性能好,主要用于替代前两种方式在小游戏工具中的高性能渲染。
   *    缺陷:只能渲染在最上层,如果想在第三方引擎层级之上再绘制,层级显示会不符合预期。这种情况在小游戏工具中应该renderByReadPixels的方式
   * 
   * 三种渲染方式中只有renderByReadPixels需要每帧都进行调用,其他两种都只需要调用一次即可。
   */
  private _render() {
    if (this._inEditor) { // 小游戏工具中有两种方式进行渲染,可以按需自行配置_useReadPixelsMethodInEditor属性
      if (this._useReadPixelsMethodInEditor) {
        this._renderByReadPixels()
      } else {
        this._renderInEditorByHudCanvas()
      }
    } else if (this._inSimulator) { // 工具模拟器中只能使用readPixels的方式进行渲染
      this._renderByReadPixels()
    } else { // 真机上只建议使用initWithCanvas的方式
      this._renderInRuntimeByInitWithCanvas()
    }
  }

  // readPixels方式将第三方渲染的结果以ArrayBuffer的形式传给texture
  private _renderByReadPixels() {
    const glCtx = /** 获取离屏canvas的webgl绘制的上下文 */ ;
    const needBufferLen = glCtx.drawingBufferWidth * glCtx.drawingBufferHeight * 4
    if (!this._tex || (this._pixels && this._pixels.length !== needBufferLen)) { // 若第一次进来,或buffer大小不一致,需要创建一段buffer,以及一个texture2d
      const tex = new engine.Texture2D();
      this._pixels = new Uint8Array(needBufferLen);

      // 用第三方引擎canvas的宽高来初始化texture2d
      tex.initDynamicTexture(glCtx.drawingBufferWidth, glCtx.drawingBufferHeight);
      const sf = engine.SpriteFrame.createFromTexture(tex);
      this._uiSprite.spriteFrame = sf;
      this._tex = tex
    }
    
    // 读取canvas上的像素值
    glCtx.readPixels(0, 0, glCtx.drawingBufferWidth, glCtx.drawingBufferHeight, glCtx.RGBA, glCtx.UNSIGNED_BYTE, this._pixels);
    // 将像素值赋值给texture2d,进行真正的渲染
    this._tex.updateSubTexture(this._pixels, 0, 0, glCtx.drawingBufferWidth, glCtx.drawingBufferHeight)
  }

  // 通过小游戏工具的hudCanvas方式进行渲染
  private _renderInEditorByHudCanvas() {
    if (!this._hasSetHudCanvas) {
      // 把第三方引擎的canvas作为渲染源
      (engine as any).Editor.API.CanvasBgManager.setCustomHudCanvas(/** 第三方引擎的离屏canvas */)
      this._hasSetHudCanvas = true
    }
  }

  // 在真机上运行时需要使用使用initWithCanvas的方式创建texture2d
  private _renderInRuntimeByInitWithCanvas() {
    if (!this._uiSprite.spriteFrame) {
      const tex = new engine.Texture2D();
      tex.initWithCanvas(/** 第三方引擎的离屏canvas */);
      const sf = engine.SpriteFrame.createFromTexture(tex);
      this._uiSprite.spriteFrame = sf;
    }
  }

  public clear() {
    if (this._inEditor) {
      (engine as any).Editor.API.CanvasBgManager.setCustomHudCanvas(null)
      this._hasSetHudCanvas = false
    }
    if (this._thirdEngineNode) {
      this.entity.transform2D.removeChild(this._thirdEngineNode.transform2D);
    }
  }
}