Cocos Creator 3D 材质系统:曲面效果如何实现?

​前不久发布的 Cocos Creator 1.0.2 版本中正式加入了对 OPPO 小游戏、vivo 小游戏以及华为快游戏平台的支持,在诸多 Creator 3D 制作的小游戏案例中,《猪猪侠:极速狂飙》已上线 OPPO 小游戏平台。

这款休闲跑酷小游戏,采用了曲面材质效果来使跑酷赛道更加多变有趣,今日,Cocos 引擎开发工程师 ChiaNing 将为各位开发者来解析这种曲面效果的实现思路和方案,在阅读完本文之后,大家便可以将这种效果应用在自己的游戏中。

使用背景

在固定背后视角的跑酷游戏中,玩家面对的始终是前方布满障碍的赛道,除去有趣的障碍设计可以一直吸引玩家的注意外,许多游戏还会添加一些视觉效果来使游戏画面看起来更丰富有趣,比如今天我们要分享的这款《猪猪侠:极速狂飙》就采用了曲面的效果使得赛道看起来更多变,也更有立体感和纵深感。

那么,这样的曲面效果在 Cocos Creator 3D 中是如何实现的呢?

实现方案分析

要实现曲面的效果,我们有几种方案可选择:

1. 直接使用曲面模型

这是最直观最容易想到的实现方案,从模型层面直接将效果做好,省去了其他处理,但这种方案也存在着很多严重的问题:

(1)模型复用不便,模型生成时的状态几乎决定了它的使用场合,这对于游戏开发中需要大量复用资源以减小包体来说有严重的问题。

(2)对于跑酷游戏这种物理需求并不复杂的游戏来说,大部分的游戏逻辑都可以直接通过计算直接完成而并不需要依赖物理引擎实现,对于正常的模型来说,规则的形状对于逻辑实现是很友好的,但是启用曲面模型就会对这种计算带来很多困难,几乎只能通过使用物理引擎来实现,过多的物理计算对性能是会有较大的影响的。

包体不友好,性能不友好,异型模型还会对制作带来麻烦,对于只是为了实现显示效果来说,这些损耗得不偿失。

2. 使用材质系统实现

我们需要明白一点,要实现的曲面的效果,实际上影响的只有显示效果,与其他的任何系统是不相关的,它不应当影响到其他无关的模块,既然只想改变显示,那采用材质系统相较于采用曲面模型的方案有着诸多好处:

(1)不必使用物理引擎,简单的物理效果可以通过计算来实现,效率更优。

(2)模型可复用,想要实现不同的弯曲效果也很方便,只要使用带有曲面效果的不同参数的材质即可实现同一模型的不同效果。相较于方案一的多重模型来说,只需要几个材质即可解决问题。

(3)参数可配置,可以通过参数调节来得到不同的效果。

分析看来,相较于直接使用曲面模型的方案来说,使用材质系统实现的方案优势很明显,没有额外的开销,也没有太大的包体负担。

综上所述,使用材质系统实现更能满足我们的需求,因此采用材质系统来实现这个效果。

方案思路分析

从需求来看,我们的目的是实现一个与我们的观察点相关的模型变形,既然只是变形,并不涉及到颜色的变化和处理,那么需要处理的就只有顶点着色器部分,并不涉及片段着色器。对于不太清楚渲染管线各个阶段的读者,可以参考 [LearnOpenGL]的渲染管线介绍。

在 Shader 中,通过顶点着色器即可完成对模型顶点位置的操作。

明确了是对顶点位置进行操作后,我们将摄像机所在的点定为原点。

由于我们的摄像机是固定在人物背后,且赛道始终保持向 Z 轴负方向延伸,所以可以将模型与摄像机的 Z 轴方向的距离看作函数的输入值,想要得到曲面的效果,模型的点的变化规律如下:

距离原点越远的点产生的偏置值越大(函数在区间内为增函数)
距离原点越远的点偏置的变化速度越快(函数的导数为一次函数)
由上述两条规律不难得出,二次函数的变化规律与我们想要实现的曲面效果的规律契合,所以我们的顶点着色器的运算为一个关于顶点位置 Z 值的二次函数运算。

我们刚刚得出的规律是建立在一个特定空间下的,即以摄像机为原点的空间,这个空间正是空间变换中的观察空间阶段,所以我们之后对顶点的操作正是在这个空间中进行才能够得到正确的结果。

材质系统简介

Cocos Creator 3D 提供了完备的材质系统,基于这套材质系统,我们能够很方便地在引擎中创建使用编辑材质,并且在场景预览窗口能够随时观察到材质更改所带来的变化。

在 Cocos Creator 3D 编辑器中与材质系统相关的资源有两种,分别为:

Effect 资源

此类型资源为符合 Cocos Effect 语法标准的渲染流程描述文件,由 YAML 格式的流程控制清单和基于 GLSL 300 ES 语法的 Shader 片段共同组成。

Material 资源

此资源可看做是 Effect 资源在场景中的资源实例,其本身除了 Effect 资源的引用外,还包括很多可配置参数以决定 Material 的状态。在实际使用中,我们的模型是需要使用 Material 资源的,这样就可以实现使用同一个 Effect 但参数不同以实现不同效果的需求了。

材质的使用也非常的方便,在 Cocos Creator 3D 编辑器的资源管理器中右键即可新建出 Effect 资源和 Material 资源。

在 Material 资源可选择需要使用的 Effect 还可对其他参数进行配置,完成配置并保存后,选中需要使用材质的模型,选中需要的 material 或者直接将 material 拖入框中即可完成材质的设置。

具体详尽的材质介绍和参数表请参看官方文档[材质系统]

具体实现

思路已经很清晰了,那么现在开始着手实现 Shader。下面是具体的实现步骤:

1、启动 Cocos Creator 3D 编辑器(以下简称编辑器),为实验方便,使用最简单的场景即可,新建场景后,在场景编辑器中新建一个 Plane 模型,之后以此对象作为查看 Shader 效果的对象。

2、在编辑器的资源管理器中右键新建 Effect,将其命名为 curved 或者符合要求的名字。

3、这时新建的 Effect 文件为编辑器内置的 Effect 模板,其包含了最基础的 shader 结构,我们需要在这个基础上添加我们需要的功能,关于 Effect 的具体介绍请参看我们的[材质 Effect 文档],在里我只对我们需要的更改做出介绍。

4、先来看 CCEffect 部分:

CCEffect %{
  techniques:
  - name: opaque
    passes:
    - vert: general-vs:vert # builtin header
      frag: unlit-fs:frag
      properties: &props
        mainTexture:    { value: white }
        mainColor:      { value: [1, 1, 1, 1], editor: { type: color } }
  - name: transparent
    passes:
    - vert: general-vs:vert # builtin header
      frag: unlit-fs:frag
      blendState:
        targets:
        - blend: true
          blendSrc: src_alpha
          blendDst: one_minus_src_alpha
          blendSrcAlpha: src_alpha
          blendDstAlpha: one_minus_src_alpha
      properties: *props
}%

在默认的 Effect 模板中,我们需要更改的是 vert 字段所使用的 Shader 片段,默认模板中提供的 general-vs:vert 是内置的顶点着色器,所以我们需要将其替换为我们即将实现的顶点着色器的名字(暂定为 unlit-vs).

接下来,需要对 properties 部分进行修改,property 列表将会将属性暴露在编辑器面板上方便我们的编辑和更改。

此时需要决定哪些数据是需要作为 uniform 传入 shader 中对效果做出影响的了,结合之前分析的需求:

需要有一个决定模型点在各个分量轴上偏置值的偏置位置信息,我们使用一个 vec4 来存储这个偏置值(allOffset);
需要有一个决定偏置变化的系数的值,使用一个 float 即可(dist);
还可以添加模型的主贴图等(mainTexture)

经过以上更改之后,Effect 的 CCEffect 部分看起来是这个样子的:

  CCEffect %{
    techniques:
    - name: opaque
      passes:
      - vert: unlit-vs:vert
        frag: unlit-fs:frag
        properties: &props
          mainTexture:  { value: grey         }
          allOffset:    { value: [0, 0, 0, 0] }
          dist:         { value: 1            }
    - name: transparent
      passes:
      - vert: unlit-vs:vert
        frag: unlit-fs:frag
        depthStencilState:
          depthTest: true
          depthWrite: false
        blendState:
          targets:
          - blend: true
            blendSrc: src_alpha
            blendDst: one_minus_src_alpha
            blendDstAlpha: one_minus_src_alpha
        properties: *props
  }%

5、由于默认的 Effect 模板中使用了内置的顶点着色器,所以这里需要实现自己的顶点着色器,可以参考内置的 builtin-unlit 的实现来编写此段 shader:

  • 添加需要的 uniform :

uniform Constants {
vec4 allOffset;
float dist;
};

  • 编写入口函数:vert

  • 按照引擎要求对接骨骼动画和数据解压,直接在开头调用 CCVertInput 工具函数。

  • 模型资源在场景中可能出现很多重复的,这样就需要对模型进行动态合批,对接引擎的动态合批流程,在包含 cc-local-batch 头文件后,通过 CCGetWorldMatrix 函数获取世界矩阵。

vec4 position;
CCVertInput(position);

      highp mat4 matWorld;
      CCGetWorldMatrix(matWorld);
  • 在分析时提到,需要在观察空间下对顶点进行处理,所以需要将坐标转换到观察空间下。曲面效果是和 Z 坐标直接相关的,所以系数也是直接影响 Z 坐标的。

  • dist 系数为影响变化的系数,所以在和 vpos.z 的运算时,可以使用乘法也可以使用除法,但这个改变会直接影响 dist 的取值,所以在决定是使用除法还是乘法后,需要对值进行对应修改,且注意使用除法时 dist 的值不可为 0。

  • 对于各轴分量的修改,需要 allOffset 参与运算然后造成影响,此处的 zOff 的平方运算即为分析中的二次函数符合变化规律的实现。

  • 在处理完成之后,按照正常的变换逻辑继续将观察空间通过投影矩阵变换为裁剪空间下的坐标之后继续传递给片段着色器即可。

highp vec4 vpos = cc_matView * matWorld * position;
highp float zOff = vpos.z / dist;
vpos += allOffset * zOff * zOff;
highp vec4 pos = cc_matProj * vpos;

      v_uv = a_texCoord;
      return pos; 

6、对于片段着色器,我们并未做特殊的操作,所以直接使用默认提供的就可以。

7、最终的 effect 如下:

 CCEffect %{
  techniques:
  - name: opaque
    passes:
    - vert: unlit-vs:vert
      frag: unlit-fs:frag
      properties: &props
        mainTexture:  { value: grey         }
        allOffset:    { value: [0, 0, 0, 0] }
        dist:         { value: 1            }
  - name: transparent
    passes:
    - vert: unlit-vs:vert
      frag: unlit-fs:frag
      depthStencilState:
        depthTest: true
        depthWrite: false
      blendState:
        targets:
        - blend: true
          blendSrc: src_alpha
          blendDst: one_minus_src_alpha
          blendDstAlpha: one_minus_src_alpha
      properties: *props
}%

CCProgram unlit-vs %{
  precision highp float;
  #include <cc-global>
  #include <cc-local-batch>
  #include <input>

  in vec2 a_texCoord;
  out vec2 v_uv;

  uniform Constants {
    vec4 allOffset;
    float dist;
  };

  highp vec4 vert () {
    vec4 position;
    CCVertInput(position);

    highp mat4 matWorld;
    CCGetWorldMatrix(matWorld);

    highp vec4 vpos = cc_matView * matWorld * position;
    highp float zOff = vpos.z / dist;
    vpos += allOffset * zOff * zOff;
    highp vec4 pos = cc_matProj * vpos;

    v_uv = a_texCoord;
    #if FLIP_UV
      v_uv.y = 1.0 - v_uv.y;
    #endif
    return pos;
  }
}%

CCProgram unlit-fs %{
  precision highp float;
  #include <output>

  in vec2 v_uv;
  uniform sampler2D mainTexture;

  vec4 frag () {
    vec4 o = vec4(1, 1, 1, 1);

    o *= texture(mainTexture, v_uv);

    return CCFragOutput(o);
  }
}% 

8、在完成了 effect 之后,我们可以在编辑器中新建一个材质,在材质的 Effect 中选择刚刚完成的 curved 之后传入想要的贴图,填入 dist 和 AllOffset 参数,保存之后将这个材质赋予刚刚提到的 plane 对象,调整参数可以看到我们的片面就能出现偏置效果了,移动摄像机可看到偏置效果是与顶点距离摄像机的距离相关的:

9、请注意,Shader 的参数与模型的尺寸是相关的,上图所示效果是在 dist 为 100,AllOffset 的 Y 值为 10 时的效果,各位开发者可尝试不同的组合来达到想要的效果。

10、似乎上图的效果还不是太直观,所以我用一些建筑模型和一些路面模型简单搭建了一段赛道来模拟游戏可能会出现的场景。建议最好是能够显示出纵深效果的连续模型段,更能显示出效果,当然这个效果并不只限于 Y 轴方向,还可以同时满足 X 轴方向的偏置需求,下图所示即为 Dist 为 100,X 为 -20,Y 为 -10 时的效果图:

总结

相信各位对 [ShaderToy] 并不陌生,之后我们将提供从 ShaderToy 中迁移 Shader 的方法,海量 Shader 等你来学习!目前在我们的[官方案例仓库]的 demo02 中,已经实现了一个迁移好的场景案例(ShaderToy),感兴趣的小伙伴们赶快去试试吧!

有很多小伙伴在社区分享了炫酷的材质效果,在这里感谢 @yans 做出的分享,欢迎有想法的开发者多做分享,共同促进社区成长。

以上就是我今天带来的材质系统案例分享,希望通过这个简单的案例为各位提供一个了解材质系统的入口。材质系统功能十分丰富,能够实现的效果也是多种多样的,各位快快打开脑洞,用 Cocos Creator 3D 来实现各种炫酷的效果吧!

36赞

前排围观

1赞

火前刘明

mark

study

卧槽,解决了我多年的困惑

mark

mark

1234

mark

mark

很有用

Mark

Mark

如图,我用了您这边的代码,于是效果如下:


可以看到远处的地板是弯曲了。

。。。
但是,他们没有连接再一起。
这个如何处理???

我是先放一段直(alloffset全0)
过了一段实际改弯道(设置allofset的值非0)
因为实际游戏要考虑各种曲线变化

最新版本,踩坑
cc-local-batch>
换成
cc_matWorld cc-global

#include input
换成
#include input-standard

如果没改会怎样?