Roystan的卡通水着色器

Roystan的卡通水着色器

铁名_IronName Lv4

https://roystan.net/articles/toon-water/

只要跟着做就行,中间没有遇到bug,简直是享受。

基于深度的颜色

深度纹理

代码块中有一行声明了一个名为 _CameraDepthTexture 的 sampler2D 。这个声明使我们的着色器能够访问一个_未在属性中声明的_变量:摄像机的深度纹理 。
_CameraDepthTexture 变量对所有着色器全局可用,但默认情况下不可用 ;如果在场景中选择 Camera 对象,你会注意到它附加了脚本 CameraDepthTextureMode ,并且其检查器字段设置为 Depth 。此脚本指示相机将当前场景的深度纹理渲染到上述着色器变量中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// CameraDepthTextureMode.cs
public class CameraDepthTextureMode : MonoBehaviour
{
[SerializeField]
DepthTextureMode depthTextureMode;

private void OnValidate()
{
SetCameraDepthTextureMode();
}

private void Awake()
{
SetCameraDepthTextureMode();
}

private void SetCameraDepthTextureMode()
{
GetComponent<Camera>().depthTextureMode = depthTextureMode;
}
}

从深度纹理获取线性深度值

1
2
3
float existingDepth01 = tex2Dproj(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPosition)).r;
float existingDepthLinear = LinearEyeDepth(existingDepth01); // 将非线性深度转换为线性深度
float depthDifference = existingDepthLinear - i.screenPosition.w;
tex2Dproj 和 tex2D 的不同

tex2Dproj 会对纹理坐标做透视除法(除以 w 分量),而 tex2D 直接使用提供的 UV 坐标。
tex2D 用于普通纹理映射(模型UV),坐标是模型顶点提供的 UV 坐标。
tex2Dproj 用于投影纹理映射(阴影贴图、贴花、聚光灯投影),坐标是从光源或摄像机空间计算出的投影坐标(如 float4(lightSpacePos, 1) 或 ComputeScreenPos(o.pos)

关于投影

想象你拿一个投影仪,把一张幻灯片投射到墙上:

  • 投影仪就是“投影摄像机”,它的镜头决定了光线投射的方向和范围。
  • 墙上某一点 P,它在投影仪的“胶片平面”上有一个对应的 2D 位置(就是 UV)。这个位置正是通过把 P 点变换到投影仪的裁剪空间,再做透视除法得到的。
  • 然后,投影仪把该 UV 处的颜色照到 P 点上。

波浪

  • 应用一个阈值来获得更接近二元效果的外观。
  • 希望波浪强度在近岸或物体与水面交汇处增强,从而产生泡沫效果 。我们将根据水深调节噪声截止阈值来实现这一效果。

动画

  • 添加动感:通过偏移用于采样噪声纹理的 UV 坐标

Edit Mode 下 Time 的更新不是实时的,所以动画看起来是卡顿的。

  • 滚动效果就像一张纸在表面上拖动一样。我们将使用扭曲纹理来增加动态感。这种扭曲纹理类似于法线贴图 ,但只有两个通道(红色和绿色),而不是三个。

渲染法线缓存

  • 根据水面法线与两者之间物体法线之间的角度来调节泡沫深度值(着色器中的 _FoamDistance )。为此,我们需要访问法线缓冲区 。

  • Unity 内置了使用 DepthNormals 深度纹理模式渲染法线缓冲区的功能。该模式会将深度缓冲区和法线缓冲区打包到一个纹理中(每个缓冲区两个通道)。然而,这样做会导致深度缓冲区的精度不足以满足我们的需求;因此,我们将手动将法线渲染到单独的纹理中。

  • 此脚本创建了一个与主摄像机位置和旋转角度相同的摄像机,但它使用替换着色器渲染场景。此外,它不会将场景渲染到屏幕上,而是将输出存储到名为 _CameraNormalsTexture 的全局着色器纹理中。与我们上面使用的 _CameraDepthTexture 一样,此纹理可供所有着色器使用。

  • 利用点积的结果来控制泡沫量。当点积较大(接近 1)时,我们将使用比点积较小(接近 0)时更低的泡沫阈值。

透明

1
2
Blend SrcAlpha OneMinusSrcAlpha
ZWrite Off
  • Blend 线决定了混合的具体方式。我们使用的是通常被称为 “普通混合”的混合算法,它类似于 Photoshop 等软件混合两个图层的方式。
     - ZWrite Off 可以防止我们的对象被写入深度缓冲区;如果_被_写入深度缓冲区,它将_完全_遮挡其后面的对象,而不是仅仅部分遮挡它们。
泡沫颜色和设置的不同

改变泡沫的颜色会产生不同的结果,红色泡沫会变成粉色,黑色泡沫会变成浅蓝色。
顾名思义,加色混合是将两种颜色混合在一起,从而产生更明亮的效果。这种方法非常适合用于发光物体,例如火花、爆炸或闪电。但我们想要将泡沫与水面混合——泡沫和水面本身都不发光,混合后的效果也不应该更亮;因此,加色混合并不适用于此。

叠加透明薄层

标准 Over 混合(Alpha Blend)和简单加法混合的核心区别在于:Over 混合考虑了前景的透明度(alpha),能实现“半透明覆盖”效果,而简单加法只是颜色相加,没有透明度概念

1. Over 混合公式(用这个)

final.rgb = top.rgb * top.a + bottom.rgb * (1 - top.a)
final.a = top.a + bottom.a * (1 - top.a)

  • 当 top.a = 1(泡沫完全不透明):final = top.rgb,泡沫完全覆盖水。
  • 当 top.a = 0.5(泡沫半透明):final = top.rgb * 0.5 + bottom.rgb * 0.5,水和泡沫各贡献一半。
  • 当 top.a = 0final = bottom.rgb,完全看不到泡沫。
    这种混合符合“叠加透明薄层”的物理直觉:颜色按比例混合,同时保留水的原始颜色。

2. 简单加法混合

1
2
final.rgb = waterColor.rgb + surfaceNoiseColor.rgb
final.a = waterColor.a + surfaceNoiseColor.a(通常忽略 alpha)
  • 泡沫颜色直接加到水上,无论泡沫的 alpha 是多少,都会提升亮度
  • 即使你想让泡沫半透明,比如 surfaceNoiseColor.rgb = (1,1,1) * 0.5,加法后也会使水变亮 0.5,而不是混合。
  • 结果通常会导致过曝、颜色失真,且无法实现“泡沫覆盖水”的视觉效果(水依然完全可见)。

抗锯齿

1
2
3
4
5
#define SMOOTHSTEP_AA 0.01

//...

float surfaceNoise = smoothstep(surfaceNoiseCutoff - SMOOTHSTEP_AA, surfaceNoiseCutoff + SMOOTHSTEP_AA, surfaceNoiseSample);

拓展(原文的 Conclusion)

深度缓冲区可用于实现任何基于距离的效果,例如雾效或扫描效果。法线缓冲区用于延迟着色 ,即在所有渲染完成_后_ ,作为后处理步骤对表面进行光照。扭曲和噪波的应用几乎是无限的,它们可以用来修改网格的几何形状,类似于高度图的工作原理。

  • 标题: Roystan的卡通水着色器
  • 作者: 铁名_IronName
  • 创建于 : 2026-04-21 14:35:35
  • 更新于 : 2026-04-21 21:09:56
  • 链接: https://blog.ironname.top/2026/Roystan的卡通水着色器/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论