# 内建节点

为了方便开发者,以及支持内建管线,目前小游戏框架已经提供了很多内置节点。

# 核心功能节点

首先是基础的核心功能性节点:

  1. class RGCameraNode extends RGNode<{}, 'Camera', {camera: BaseCamera}>:传入一个camera,输出一个Camera类型的数据,一般作为一个链路的开始。

  2. class RGCullNode extends RGNode<{camera: 'Camera'}, 'MeshList', {lightMode: string,initSize?: number}>:剔除节点,需要一个Camera类型的输入,提供一个lightMode作为初始化参数,最后输出这个相机的平截体中满足这个lightMode需求渲染体列表。

  3. class RGGenRenderTargetNode extends RGNode<{}, 'RenderTarget', {createRenderTarget(): RenderTexture | Screen;}>:渲染目标节点,用于创建一个渲染目标RenderTarget的输出,通过createRenderTarget方法,可以使得用到的时候才真正创建,节省内存。

  4. class RGClearNode extends RGNode<{camera: 'Camera', renderTarget: 'RenderTarget'}, 'None', {}>:清屏节点,输入一个Camera和一个RenderTarget,使用前者对画布进行清屏。

  5. class RGGenViewNode extends RGNode<{}, 'Camera', {viewObject: {view: Kanata.View}}>:清屏操作本质和相机无关,只需要一个View提供清屏的信息,有时候开发者需要在一开始对整个画布清一次,就可以使用这种方案。

有了以上节点,配合生命周期,我们可以实现一个简单的清屏:

class MyRenderGraph extends engine.RenderGraph {
  public onCamerasChange(cameras: engine.BaseCamera[]) {
    const camera = cameras[0];
    const cameraNode = this.createNode<engine.RGCameraNode>(`camera`, RGGenViewNode, {camera});
    const rtNode = this.createNode<engine.RGGenRenderTargetNode>('rt-screen', RGGenRenderTargetNode, {createRenderTarget: () => camera.renderTarget});
    const clearNode = this.createNode<engine.RGClearNode>('clear', RGClearNode, {});
    this.connect(cameraNode, clearNode, 'camera');
    this.connect(rtNode, clearNode, 'renderTarget');
  }
}

# 渲染节点

渲染节点是一种特殊的核心功能节点,由于开发者对渲染的定制性较强,所以将其抽象为了一个可以继承定制的节点:

interface IRGRenderNodeOptions<TInputs> {
  createUniformBlocks(): Kanata.UniformBlock[];
  lightMode: string;
  inputTypes?: TInputs;
  uniformsMap?: {
    [uniformName: string]: {
      inputKey: keyof TInputs,
      /**
       * 如果 uniform 是`RenderTexture`,使用哪个缓冲。
       */
      name: 'depth' | 'stencil'| 'color',
    }
  };
}

class RGRenderNode<TInputs extends {
  [key: string]: keyof IRGData;
} = {}> extends RGNode<
  TInputs & {camera: 'Camera', renderTarget: 'RenderTarget', meshList: 'MeshList'},
  'RenderTarget',
  IRGRenderNodeOptions<TInputs>
>

其中最重要的就是其初始化参数,createUniformBlocks可以让开发者自己定义一组全局的Uniform(但由于限制,目前只支持一个!),这个和前面章节渲染系统说到的不矛盾,可以认为这里的是最优先的。然后是lightMode参数,这个和详见效果和材质

其次是输入参数,渲染节点至少需要相机camera(通常来自于RGCameraNode节点)、渲染目标renderTarget(通常来自于RGGenRenderTargetNode节点)和渲染列表meshList(通常来自于RGCullNode节点)才能进行渲染,而初始化参数中的inputTypesuniformsMap则是提供了一个切口,使得开发者可以自己追加输入并将这些输入以一个便捷的方法和全局Uniform绑定起来。

除了这些之外,如果开发者还需要更加深入的定制,渲染节点还提供了一个生命周期:

public onRender(context: RenderSystem, options: IRGRenderNodeOptions<TInputs>);

绝大部分情况下开发者都不需要定制这个生命周期,如果需要使用这个生命周期,需要参考深入管线

不过要注意对于阴影的绘制,专门提供了一个节点RGLightNode,其参数和RGRenderNode完全一致,但是用于绘制阴影的,一般在使用时,会将其输出作为渲染节点的追加输入:

const fwRenderNode = this.createNode<RGRenderNode<{shadowMap: 'RenderTarget'}>>('fb-render', RGRenderNode, {
  lightMode: 'ForwardBase',
  createUniformBlocks: () => [this._fbUniforms!],
  // 追加阴影贴图的输入
  inputTypes: {'shadowMap': 'RenderTarget'},
  // 阴影贴图的输入到全局 Uniform 的映射
  uniformsMap: {
    u_shadowMapTex: {
      inputKey: 'shadowMap',
      name: 'color'
    }
  }
});
if (camera.shadowMode !== Kanata.EShadowMode.None) {
  const scRenderNode = this.createNode<engine.RGLightNode>('shadow-caster', RGLightNode, {
    lightMode: 'ShadowCaster',
    createUniformBlocks: () => [this._scUniforms!]
  });
  this.connect(scRenderNode, fwRenderNode, 'shadowMap');
}

# 天空盒节点

天空盒节点class RGSkyBoxNode extends RGNode<{}, 'MeshList', {}>很简单,其往往也是针对于camera上的drawSkybox参数,一般在使用时,假如采用 camera 为源头构建 RenderGraph 时:

if (camera.drawSkybox) {
  const skyboxNode = this.createNode('skybox', engine.RGSkyboxNode, {});
  this.connect(skyboxNode, renderNode, 'meshList');
}

即将其直接连接到渲染节点,作为渲染节点的渲染列表输入即可。

# UI节点

# 光照节点

阴影和多光源是十分常见的光照效果。在小游戏框架中,以 ShadowMap 的方式实现阴影,通过多 Pass 完成多光源的绘制。

# 阴影节点

在管线中接入阴影节点,需要经历三个步骤:

  1. 构建阴影贴图的 RenderTexture 渲染目标节点RGGenRenderTargetNode
  2. 构建阴影贴图的渲染节点RGLightNode
  3. 将阴影贴图作为参数传给场景的渲染节点

完整管线代码如下所示:

if (camera.shadowMode !== Kanata.EShadowMode.None) {
    // 构建阴影贴图的RenderTexture
    const scRTNode = this.createNode<RGGenRenderTargetNode>('sc-render-target', RGGenRenderTargetNode, {createRenderTarget: () => this._createRT(camera, 'ShadowCaster')});
    // 构建阴影的渲染节点
    const scRenderNode = this.createNode<RGLightNode>('shadow-caster', RGLightNode, {
      lightMode: 'ShadowCaster',
      createUniformBlocks: () => [this._scUniforms!]
    });
    
    // 将相机节点、渲染列表与阴影节点连接,将阴影节点与场景渲染节点连接
    this.connect(cameraNode, scRenderNode, 'camera');
    this.connect(scRTNode, scRenderNode, 'renderTarget');
    // fwCullNode输出场景对象的渲染列表
    this.connect(fwCullNode, scRenderNode, 'meshList');
    // 将阴影贴图作为参数传给场景的渲染节点
    this.connect(scRenderNode, fwRenderNode, 'shadowMap');
  }

这样,我们可以开始渲染阴影

# 多光源节点

在管线中接入多光源渲染的节点,需要经历四个步骤:

  1. 构建光源裁剪节点RGCullLightFANode,对视阈内对象没有影响的光源将被剔除
  2. 构建渲染物体的裁剪节点RGCullMeshFANode,视阈内不受多光源影响的对象将被剔除
  3. 构建多光源的渲染节点RGFARenderNode,该节点执行对象在附加光源下的渲染
  4. 将上述节点与管线连接

完整的管线代码如下所示:

// 构建光源裁剪节点
const faCullLightNode = this.createNode<RGCullLightFANode>('fa-cull-light', RGCullLightFANode, {});
// 构建渲染对象的裁剪节点
const faCullMeshNode = this.createNode<RGCullMeshFANode>('fa-cull-mesh', RGCullMeshFANode, {});
// 多光源渲染节点
const faRenderNode = this.createNode<RGFARenderNode>('fa-render', RGFARenderNode, {
    lightMode: 'ForwardAdd',
    createUniformBlocks: () => [this._faUniforms!],
});
// 在管线中连接
this.connect(fwCullNode, faCullLightNode, 'meshList');
this.connect(fwCullNode, faCullMeshNode, 'meshList');
this.connect(faCullLightNode, faCullMeshNode);

this.connect(fwRenderNode, faRenderNode);
this.connect(cameraNode, faRenderNode, 'camera');
this.connect(rtNode, faRenderNode, 'renderTarget');
this.connect(faCullMeshNode, faRenderNode, 'meshList');

这样,我们可以开始使用多光源

# Gizmos节点

如果开发者想要在自定义管线中使用 Gizmos 协助调试,需要添加相关节点,这个操作实际上很容易。首先,camera上的drawGizmo参数明确了该相机是否绘制Gizmo。其次,class RGGizmosNode extends RGNode<{ camera: "Camera" }, "MeshList", {}>节点能够构建相应的渲染列表。最后,我们需要一个RGRenderNode渲染节点进行绘制。完整的 Gizmo 管线如下所示:

  if (camera.drawGizmo) {
    // GizmosNode构建待绘制的渲染列表
    const gizmoNode = this.createNode<RGGizmosNode>('gizmo', RGGizmosNode, {});
    // Gizmo的渲染节点
    const gizmoRenderNode = this.createNode<RGRenderNode>('gizmo-render', RGRenderNode, {
      lightMode: 'ForwardBase',
      createUniformBlocks: () => [this._fbUniforms!],
    });
    // Gizmo的视图节点
    const rect = camera.viewport;
    const x = rect ? rect.xMin : 0;
    const y = rect ? rect.yMin : 0;
    const width = rect ? rect.width : 1;
    const height = rect ? rect.height : 1;
    const viewPortRect: Kanata.IRect = {x, y, w: width, h: height};
    const scissorRect: Kanata.IRect = {x, y, w: width, h: height};
    const gizmoView = new Kanata.View({
      passAction: {
        depthAction: Kanata.ELoadAction.CLEAR,
        clearDepth: 1,
        colorAction: Kanata.ELoadAction.LOAD,
        stencilAction: Kanata.ELoadAction.LOAD,
      },
      viewport: viewPortRect,
      scissor: scissorRect
    })
    // 在画 Gizmo 之前,将深度清除,保证 Gizmo 画在最前面
    const gizmoClearDepthNode = this.createNode<RGClearNode>('gizmo-clear-depth', RGClearNode, {})
    const gizmoViewNode = this.createNode<RGGenViewNode>('gizmo-view', RGGenViewNode, { viewObject: { view: gizmoView } })

    // 连接 Gizmo 节点和Camera、RT节点
    this.connect(rtNode, gizmoClearDepthNode, 'renderTarget');
    this.connect(gizmoViewNode, gizmoClearDepthNode, 'camera')
    this.connect(gizmoClearDepthNode, gizmoRenderNode);

    this.connect(cameraNode, gizmoNode, 'camera');
    this.connect(gizmoNode, gizmoRenderNode, 'meshList');
  } 

成功构建完自定义渲染管线之后,或者使用框架默认的渲染管线,我们可以开始使用 Gizmos 相关功能

# 后处理节点

详见后处理系统

# 子图节点

子图节点RGSubGraphNode用于传入一个图的类,构建一个子图,这个节点的输入将会连接到子图的第一个节点,输出将会连接到子图的最后一个节点,所以需要用户保证输入输出的唯一性。

type TForwardBaseSubGraphNode = RGSubGraphNode<
  RGCameraNode['inputTypes'],
  RGRenderNode['outputType'],
  IForwardBaseSubGraphOptions
>;

const subGraph = this.createNode<TForwardBaseSubGraphNode>(`subGraph-${camera.entity.name}`, RGSubGraphNode, {
  RGClass: ForwardBaseSubGraph,
  options: {camera}
});

以上代码便是通过自定义的子图ForwardBaseSubGraph和参数camera创建了一个子图节点,这个子图的输入是一个相机的输入(第一个节点时相机节点),输出是渲染节点的输出(最后一个节点是渲染节点)。