cocos shader 用混合测试实现玻璃窗效果

混合测试(Blend test)

我们常说的混合针对的是物体透明度处理的一种技术。比如现实生活中的玻璃窗,我们能通过透明度看穿物体。透明物体分为完全透明(让所有颜色通过,透明度为 0)和半透明(让颜色通过,同时也会带有自身的颜色)。透明度通过 alpha 值来决定,也就是颜色(rgba)的 a 分量。当一个物体的 alpha 为 0.5,代表物体的颜色有 50% 来自于自身,有 50% 来自于背后物体的颜色。

对于大多数 3D 物体来说,采用的都是不透(透明度为 1)的贴图。它们只需要根据深度检测决定最终的片段呈现的是哪一个物体的即可,物体深度与深度缓冲中的深度根据指定对比函数进行对比测试,测试失败的片元都会被丢弃。而透明的物体单纯用深度测试这一套规则就不合理,容易出现透明物体渲染错误的现象。所以,我们需要告诉 WebGL 如何处理带有 alpha 信息的纹素(第四章有提到过,是组成纹理图像的像素)。

透明度测试

在前面的内容我们也说过,透明物体分为完全透明和半透明。如果有些图片并不需要半透明,只需要根据纹理颜色值显示一部分,或者不显示一部分,没有中间情况,比如下图。


 

从图中可以看出,最上方有一部分是透明纹素,在实际应用中,如果不对透明纹素进行处理的话,默认就会取纹素自带的颜色呈现,如下图:


 

如果要解决上述显示错误的问题让纹素的透明信息被识别,只需要设定一个阈值,将阈值与纹理 alpha 值进行比较,丢弃掉阈值限制内的片段(像素)就好。GLSL 给了我们 discard 命令,一旦被调用,它就会保证片段不会被进一步处理。


vec4 frag () {
  vec4 col = mainColor * texture(mainTexture, v_uv);
  // 在这之前,设定一个 uniform alphaThreshold 做为阈值,阈值默认为 0.1,阈值越大,被剔除掉的像素可能会越多
if(col.a < alphaThreshold){
    discard;
  }
  return col;
}
 

透明度混合

虽然丢弃片段对我们来说很轻松,但是更多时候,我们是希望看到透明物体之间相互作用的效果。它无法像渲染不透明物体和完全透明物体一样,直接丢弃片段,而是在必要情况下,需要同时渲染多个透明度级别的图像。因此,需要启用混合(Blending)。启用混合时需要关闭深度写入,在上一章的内容有提到过,进行深度检测时,会对相对应的部分进行替换,所以是一个非 A 即 B 的操作。

混合方式始终遵循着如下方程实现:

color(RGBA) = ((sourceColor * sfactor) + (destinationColor * dfactor)). RBGA

  • sourceColor:源颜色向量。这是源自纹理的颜色向量。
  • destinationColor:目标颜色向量。这是当前储存在颜色缓冲中的颜色向量。
  • sfactor:源因子值。指定了 alpha 值对源颜色的影响。
  • dfactor:目标因子值。指定了 alpha 值对目标颜色的影响。

在混合中,最常用的跟深度测试一样,也有一个混合函数 “void gl.blendFunc(GLenum sfactor, GLenum dfactor);”

  • sfactor:源混合因子。默认值是 ZERO
  • dfactor:目标混合因子。默认值是 ZERO

如果需要分开设置,也可以采用 "void gl.blendFuncSeparate(srcRGB, dstRGB, srcAlpha, dstAlpha);" 分开设置(由于每一个参数名已经足够清晰,此处不再增加说明)。

以上,所有的因子值范围为 [0, 1],涉及到常量部分,需要通过 “void gl.blendColor(red, green, blue, alpha);” 设置,每一个分量值的范围也是 [0, 1]。

接着,尝试在 Cocos Creator 3.x 里进行实践。导入一张带有透明度的图片,我这里使用的图片如下,在图片的左下角标明了图片每一个格子的透明度。


 

接着,将图片导入应用到两个面片上,看起来像这样(此处仍然使用了完全透明测试,丢弃了 0% 格子的片段)。


 

此时可以观察到,其余带有透明度的格子仍然像没有透明度的格子一样呈现,这就是没开启混合的效果,接着,尝试开启混合,跟上一章的深度测试一样,混合测试也有一个专属状态 blendState 用来设置。在这选择采用纹理提供的透明度,因此将源因子 blendSrc 选用 SRC_ALPHA,目标因子 blendDST 选用 ONE_MINUS_SRC_ALPHA。


 

此处的计算方程表示为(这里的源使用 50%  透明度区域(0.9, 0, 0.6, 0.5),颜色缓冲假设是纯绿色(0, 1, 0, 1)):

  • color(RGB) = (0.9, 0, 0.6) * 0.5 + (0, 1, 0) * (1 - 0.5)
  • color(A) = 0.5 * 1 + 1 * 0

最终就能看到下面这种类似玻璃窗的效果:


 

但是在这里,大家有没有发现什么不对劲的地方?为什么有了混合效果之后,立方体底面和背面都不见了?这就涉及到下面要讲解的面剔除部分。


 

在默认情况下,混合方程里采用的是源和目标相加运算,我们也可以通过 “void gl.blendEquation(mode);” 来修改计算方式。可以是相加、相减或者反过来相减等,具体请查看相关 API,此处不再过多赘述。

渲染顺序

学习了深度测试和混合测试之后,我们了解到不透明物体会根据深度丢弃片段,而透明物体开启混合测试后会与颜色缓冲中的片段混合。这时候我们如果尝试着往场景里放更多的对象,比如先渲染了一个半透明物体(半透明物体距离相机更近),再渲染一个不透明物体(不透明物体距离相机更远),此时,就有可能出现这样一种情况:

右边的第二扇窗户左下角的区域看不见了,这在我们看来就显得很违和,这时候我们就可以试想一下,需要进行混合的物体要开启深度测试或者执行深度写入吗?不 透明物体和透明物体的渲染顺序应该是怎样的?

其实正确的做法大致原则如下:

  • 先绘制所有不透明的物体;
  • 对所有透明的物体排序,采用的是观察者视角,即距离相机的远近,“大多数”情况下,不开启深度测试;
  • 按照从远到近的顺序绘制透明物体。

Cocos Creator 3.x 版本也是遵循着这样的原则绘制。但针对第二点,我重点标注了一下“大多数”,那是因为还会有特殊情况。比如两个带有穿插效果的半透明物体,物体 A 的上半部分离相机更近,物体 B 的下半部分离相机更近,它们之间成 X 形状,所以会出现物体 A 的下半部分被物体 B 的下半部分遮挡,物体 B 的上半部分也是一样, 但是相机识别的是物体的深度,所以,最终就会出现一个物体表现出现异常的情况。这种状况无法避免,最理想的肯定是引擎能做网格分离,然后这个时候,半透明物体进行深度检测分离数据,但可以不进行深度写入,这样就不会丢弃片段。但即使我们如此的理想,也还是会有一些条件下无法满足需求,因此,只能在制作上尽量避免这样的状况发生,项目视情况而定了。

面剔除

既然一个物体是透明的,就意味着既能透过它看到后面的场景,也能看到它内部的结构,但现实情况好像并不是这么回事。这是因为引擎默认剔除了渲染背面,所以无论我们怎么旋转都能看到物体的正面而无法看到它背面的样子,因此,我们需要同时开启正面和背面的渲染。在这,我们可以通过一个 cullMode 来设置,cullMode 在 rasterizerState 状态设置下。

  • BACK:剔除背面
  • FRONT:剔除正面
  • NONE:不剔除

在这里,我们选择 NONE 模式,不剔除任何一个面,这时候就看到了物体的背面。


 

当然,面剔除的设置方式很简单,但肯定也会有同学想问,这是什么原理呢?简单地说,它的核心原理就是环绕顺序

环绕顺序

当我们在定义一个三角形顶点的时候,还记得之前定义了一组顶点索引数据吗?这个顶点索引数据的设置其实是有讲究的。默认底层将逆时针方向定义为正方向,因此,提供的顶点索引顺序按照逆时针方式提供即可。如果提供顺时针,则会像此处一样被剔除而无法看见。


 

>注意:此处判断逆时针还是顺时针是按照提供的渲染数据而不是观察者视角。
实现搓牌效果可以使用Cocos Creator的js shader来完成。首先,我们需要创建一个自定义的shader,将其应用到牌的渲染组件上。可以通过编辑顶点和片段着色器来实现搓牌的效果。 首先,定义一个变量来控制搓牌的进度。可以通过在脚本中设置一个初始值,然后在每一帧更新该值,以模拟手指滑动搓牌的效果。 然后,在顶点着色器中,对牌的每个顶点进行偏移操作。根据搓牌的进度值,调整顶点的位置,使之呈现出搓牌的形态。可以使用简单的插值方法(如线性插值),计算出每个顶点新的位置。 接下来,在片段着色器中,根据顶点的位置信息,设置每个像素的颜色值。可以使用纹理坐标来采样牌的纹理图片,根据搓牌的进度值,调整采样的坐标,以实现搓牌的效果。 最后,在每一帧更新搓牌的进度值,并将其传递给shader,更新牌的渲染效果。可以通过设置shader的uniform变量,将进度值传递给shader。 总结起来,使用Cocos Creator的js shader实现搓牌效果的步骤如下: 1. 创建自定义的shader,定义搓牌的进度变量。 2. 在顶点着色器中根据进度值调整顶点位置。 3. 在片段着色器中根据进度值调整纹理采样坐标。 4. 在每一帧更新进度值,并将其传递给shader。 5. 应用shader到牌的渲染组件上,实现搓牌效果。 希望这个简单的解答对你有帮助!如果你还有其他问题,请随时提问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值