今天我们来学习场景相交检测

现实中的物体如果发生相交就会在两者的接触面会有阴影,例如你的手机放在桌面上可以看到手机和桌面的黑色阴影接触线,这个在unity中同样也是可以实现的

image-20260531143806992

当着色器运行时它只能直接访问当前正在渲染的像素信息包括其在世界空间中的位置,我们想将该位置与在这个物体后面渲染的对象的位置进行比较,如果两点之间的距离小于我们指定的阈值,就检测到了一个交点

unity仅在之后将深度缓冲区的状态保存到深度纹理中,会先渲染所有不透明物体,再渲染所有透明物体,而着色器无法检测到任意两个透明物体之间的交叉

之前我们讲深度纹理的时候,提到过这个游戏场景是怎么渲染到摄像机的,也就是裁剪空间

​ image-20260531144817077

unity随后通过根据相机的参数将一些裁剪空间坐标转换为屏幕空间坐标

​ image-20260531144850095

​ image-20260531145519427

而这个向量第四个分量等于相机与被渲染顶点之间的距离

然后就是其次坐标的概念(我也没看懂这个东西,这玩意就是大一的时候没学明白的线性代数的知识)

其次坐标

一句话定义

齐次坐标 = 用 N+1 个分量来表示 N 维空间中的点/向量

维度普通坐标齐次坐标
2D(x, y)(x, y, w)
3D(x, y, z)(x, y, z, w)
4D(x, y, z, w)(x, y, z, w, v)

为什么要多一个 w?

核心目的:用矩阵乘法统一表示平移、旋转、缩放

普通 3×3 矩阵只能做旋转和缩放,无法做平移:

[abcdefghi][xyz]=旋转/缩放后的结果\begin{bmatrix} a & b & c \\ d & e & f \\ g & h & i \end{bmatrix} \begin{bmatrix} x \\ y \\ z \end{bmatrix} = \text{旋转/缩放后的结果}

但平移是加法:newPos = pos + offset,矩阵乘法只有乘法没有加法。

齐次坐标的解决方案

把 3D 点变成 4D,w=1:

[R11R12R13TxR21R22R23TyR31R32R33Tz0001][xyz1]=[R⋅p⃗+T⃗1]\begin{bmatrix} R_{11} & R_{12} & R_{13} & T_x \\ R_{21} & R_{22} & R_{23} & T_y \\ R_{31} & R_{32} & R_{33} & T_z \\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix} = \begin{bmatrix} R \cdot \vec{p} + \vec{T} \\ 1 \end{bmatrix}

最后一列就是平移量! 一个 4×4 矩阵同时搞定了旋转、缩放、平移。


w 的两个关键作用

  1. w = 1 → 点(Point)

可以平移:

text
1
[x, y, z, 1]  ×  矩阵  =  [x', y', z', 1]
  1. w = 0 → 向量(Vector)

不能平移(方向不变):

text
1
[x, y, z, 0]  ×  矩阵  =  [x', y', z', 0]

向量代表方向,平移没有意义。w=0 时矩阵的平移分量会被消掉。


投影变换:w 的第三个作用

透视投影需要除法,但 GPU 的顶点着色器输出的是 4D 齐次坐标。w 在这里存储了透视除法的分母:

text
12345
裁剪空间输出: (x, y, z, w)
↓
透视除法: (x/w, y/w, z/w)
↓
NDC 空间: (-1~1, -1~1, 0~1)

w 越大(物体越远),除完后坐标越小,实现近大远小的透视效果。


在 Unity Shader Graph 中的体现

节点含义
Position (Object)(x, y, z, 1) — 模型空间点
Position (World)经过 Model 矩阵变换后的点
Position (Clip)经过 MVP 矩阵后的齐次坐标 (x, y, z, w)
Screen Position经过透视除法后的 (x/w, y/w)

一句话总结

齐次坐标用多一个 w 分量,把"平移"塞进矩阵乘法里,同时让透视投影的除法有地方存分母。 是 3D 图形管线能高效运行的数学基础。

我们添加一个scene depth节点来获取相机和物体之间的距离,并设置为eye模式来获取精确值,

image-20260603151708188

Scene Depth 从深度纹理中读取当前屏幕位置记录的深度值,表示:

沿着当前像素对应的视线方向,场景中已经写入深度缓冲区的表面距离摄像机有多远

参数全称含义数值范围核心特点
EyeEye-space depth非线性,近处密集、远处稀疏相机空间的真实深度值,非线性分布
Linear01Linear 0-1 depth[0, 1]线性映射到 [0,1],0=近裁剪面,1=远裁剪面
RawRaw depth buffer value[0, 1] 或平台相关深度纹理的原始存储值,通常是反向Z的非线性值

然后添加screen position节点并设置为Raw模式,

Screen Position 返回当前着色器像素在屏幕上的位置,以多种坐标系形式输出,返回当前像素的屏幕空间位置 (x,y,z,w)

对于 Raw 模式下的 Screen Position,w 可以用于获得当前像素相对于摄像机的深度信息(Eye Depth)。

用于:

  • 采样屏幕纹理(如 Scene Color、Scene Depth)
  • 制作基于屏幕坐标的特效(扫描线、故障艺术、径向模糊中心点)
  • 实现与分辨率无关的 2D 效果
模式坐标系返回值范围核心特点
Default屏幕左下角原点归一化 UV (x, y, z, w)x,y: [0, 1]最常用,采样屏幕纹理的标准 UV
Raw裁剪空间齐次坐标 (x, y, z, w)未归一化,含透视除数 w手动做透视除法,极少用
Center屏幕中心原点归一化坐标 (x, y, z, w)x,y: [-1, 1]径向特效、对称遮罩
Tiled平铺模式自动平铺的 UV (x, y, z, w)重复平铺,范围无限制网格、重复图案
Pixel屏幕像素坐标实际像素位置 (x, y, z, w)x,y: [0, 屏幕宽高]像素级精确控制

然后用split节点分出第四个分量w

还记得之前的这个图吗

​ image-20260531143806992

mermaid
1234567891011
graph LR

Camera["📷 Camera"]

Sphere["🔵 当前正在绘制的物体"]
Floor["⬜ 地面(已经渲染到Depth Buffer)"]

Camera -->|"Current Depth = 8"| Sphere
Camera -->|"Scene Depth = 10"| Floor

Sphere -->|"Depth Difference = 10 - 8 = 2"| Result["Intersection Depth"]

SceneDepth = 后面的表面距离摄像机多远

ScreenPosition.w = 当前表面距离摄像机多远

不过刚才的那几个节点我们要重复使用,我们可以使用子图来实现,类似于一个函数,只暴露输入口和输出口

子图

​ image-20260531150723716

我们可以使用子图,这里理解一下子图的概念

Unity Shader Graph 的子图(Sub Graph)是一种模块化的着色器构建方式,允许你将一组节点封装成可复用的资产,然后在主 Shader Graph 或其他子图中调用它。这类似于编程中的函数或类,能显著提升复杂着色器的可维护性和复用性

子图是一个独立的 .shadersubgraph 资产,包含:

  • 输入端口(Input):定义外部传入的数据(如颜色、向量、纹理采样结果)+
  • 内部节点网络:实现特定功能(如边缘光、顶点动画、噪声混合)
  • 输出端口(Output):返回处理后的结果

创建方式

  • 在asset窗口里右键 Create → Shader Graph → Sub Graph
  • 或在 Shader Graph 编辑器中选中一组节点 → 右键 Convert to Sub-graph

​ image-20260603154023234

我们选中这些节点然后将这几个合并为子图,放在项目的任意位置

如图我们已经创建成功

​ image-20260603154307311

添加控制器

双击就可以进入子图

​ image-20260603154331947

由于这个子图没有输入,但是必须输出,所以我们可以创建一个浮点输出类型取名为out

​ image-20260603154756669

然后我们将模式设置transparent透明,然后将子图连接到基础色上

​ image-20260603160253743

将其运用到小球上,可以注意到交界处有黑色阴影

​ image-20260603160443462

此时说明已经可以检测交叉处状态了,但是我们需要当球体远离交叉处时阴影逐渐降低

我们添加One minus节点(这个我们之前用过)来实现远离时值的降低,但是由于远离的时候的值会变成负数,所以我们需要这个Saturate节点来将负数值设置为0,超过1的值设为1

然后我们需要控制这个渐变值 首先添加一个intersection power的float类型用来控制,然后将其变成默认值为1 范围0.01~25的slider滑块,0会完全遮挡,但是25的遮挡效果非常微弱

为了更方便的调整遮挡效果的明暗度,我们添加一个Occlusion strength值来控制明暗度,这样要控制明暗度就可以直接控制这个occlusion就可以了

然后用lerp 节点线性插值来控制颜色和强度,T槽导入颜色的强度,A槽为设置的基础色的颜色,B槽添加一个float节点设置为0代表黑色,这样就可以让颜色混合更自然

​ image-20260603162910443

这里温习一下这几个节点

① One Minus 节点就是1 减去输入值,即:

plain

plain
1
输出 = 1 - 输入

数学本质

输入输出
01
0.250.75
0.50.5
0.750.25
10

效果:将数值"翻转"——黑的变白,白的变黑,0 变 1,1 变 0。

② Lerp 节点就是线性插值(Linear Interpolation),在 Shader Graph 中用于在两个值之间按百分比混合。

数学公式

plain

plain
12
Lerp(A, B, T) = A + (B - A) × T
              = A × (1 - T) + B × T
T 值结果
0完全 A
0.2575% A + 25% B
0.5A 和 B 各一半
0.7525% A + 75% B
1完全 B

三个端口

端口名称作用
A起始值T=0 时的输出
B结束值T=1 时的输出
T插值因子控制混合比例,范围通常 0~1

③ Saturate 节点就是把输入值限制在 [0, 1] 范围内,超出的部分被截断。

数学公式

plain

plain
12345
Saturate(x) = clamp(x, 0, 1)

x < 0  → 输出 0
0 ≤ x ≤ 1 → 输出 x(不变)
x > 1  → 输出 1

输入输出对照

输入输出
-0.50
00
0.30.3
0.50.5
0.80.8
11
1.51
21

这里是**普通材质(左)和我们场景相交材质(右)**的区别,可以看到右侧的融合的更自然,而且很有原神这些二游那味

​ image-20260603162438341

比如我们的小球现在只漏出来一点点

​ image-20260603195615167

然后露出来很多

​ image-20260603195644722

这样就很直观了

海水波浪泡沫效果

还记得之前我们做过一个波浪图吗?

​ image-20260603201730525

这里我们需要复制一个来复用

我们可以为这些属性分组

​ image-20260603202602329

上一篇我们知道这个场景相交必须是透明transparent材质,所以我们设置为透明材质

​ image-20260603202732721

之前的节点都保持不变

​ image-20260603215246881

我们先把上一篇我们做的DepthIntersection节点插入(还记得这个是干什么的吗? 这个是用来计算相交的阴影逻辑的),

我们用这个深度交叉值使用divide除法节点除以我们自定义的float类型的Form distance(泡沫距离)参数,这个参数设置为默认值为1范围0~5的slider,

然后我们用之前的step阶梯函数节点做一个限制,将edge值(x)设置为1(也就是当深度值低于某个阈值时,这组节点才会输出1)

​ image-20260603223027770

​ image-20260603223059414

然后再添加一个color节点来控制泡沫颜色,当然还是要和基础色相加来控制海浪的颜色,此时Basecolor组用split 来分出alpha阿尔法值进行控制透明度

这里复习一下之前学的Step节点

Step 节点就是阶梯函数/阶跃函数,根据阈值把输入分成两部分:0 或 1。

数学公式

plain
1234
Step(Edge, In) = In >= Edge ? 1 : 0

输入 ≥ 阈值 → 输出 1
输入 < 阈值 → 输出 0

两个端口

端口名称作用
Edge阈值/边缘分界点,默认 0.5
In输入值要被判断的数值

​ image-20260603221356612

很好,我们已经看到其他模型和水材质接触的时候有白色的交叉面了

但是,我们也不希望我们的游戏里的水面效果如此简单吧(我记得以前玩的很多小游戏水面效果就是这样,当然也不是不能玩)

所以至少要真实一点点就像这样

不过由于我们没有高精度水面模型况且是在URP下操作的,所以水面波浪就这样吧

​ image-20260603223523731

噪波效果

在 Unity Shader Graph 的 Simple Noise 节点中,Deterministic 和 Legacy Sine 是 Hash Type(哈希类型) 下拉菜单中的两个选项,它们决定了节点生成伪随机值(即噪声)时使用的哈希函数

Scale 值效果
较小(如 1~5)噪声图案更大、更稀疏,单个噪声"斑点"尺寸大,过渡平缓
较大(如 50~100)噪声图案更细小、更密集,高频细节丰富,纹理更细碎
非常大(如 500+)可能开始出现重复或伪影(因为哈希精度限制),图案趋于混乱
选项说明适用场景
Deterministic使用一个跨平台一致的哈希函数,确保在不同 GPU 和平台上生成完全相同的噪声结果。这是 Unity 2021.2 之后的默认选项。需要跨平台一致性、可复现结果的项目
Legacy Sine使用基于 sin() 函数的传统哈希函数。这个方式在不同 GPU 上可能产生差异,某些集成显卡甚至会出现明显的块状伪影或完全错误的结果。旧项目兼容
image-20260603223712307

噪波可以让原本不自然的效果变得逼真自然,很多游戏的地图的生成也是用波函数坍缩实现的,在这个simple noise节点中有两个输入,UV用于噪声的坐标,不过我们可以使用任意的vector2的类型,另一个scale是比例尺用于控制噪声大小,这里添加一个Foam scale浮点类型默认值为500

为实现动态噪点,我们可以添加一个Foam velocity的vector 2值,然后和时间相乘

​ image-20260603225457891

然后在step中接入

​ image-20260603230318701

然后我们复习一下这几个节点

① Time 节点提供时间值,让材质动起来

输出端口说明典型用途
Time自场景加载以来的总时间(秒),持续递增持续循环的动画,如流动的水、飘动的云
Sine Timesin(Time),范围 -1 ~ 1,周期性波动平滑的往复运动,如呼吸灯、摆动
Cosine Timecos(Time),范围 -1 ~ 1,与 Sine 相位差 90°与 Sine 配合做圆周运动或交替效果
Delta Time上一帧的耗时(秒),每帧不同帧率无关的动画、物理模拟、平滑插值
Smooth Time经过平滑处理的时间,变化更柔和需要缓动效果的时间驱动动画

② Tiling And Offset 节点控制纹理的平铺次数和位置偏移

输入作用
UV原始 UV 坐标(通常直接连 UV 节点)
Tiling平铺次数,默认 (1,1)。比如 (2,2) 就是 2×2 重复
Offset偏移量,默认 (0,0)。比如 (0.5, 0) 让纹理横向移动一半

我们把材质运用上

​ image-20260603230657612

可以看到此时的泡沫就很真实了

但是可以注意到边缘没有泡沫,这不符合逻辑

​ image-20260605130856147

这里是因为子图的逻辑是用物体的遮挡背后的地面来渲染出来阴影也就是泡沫,但是没有物体遮挡的地方就没有影子,所以我们需要修改一下子图的节点

我们之前的子图里的scene depth节点的左侧有一个默认的小节点default,

这个等同于默认模式下(default)的屏幕位置节点(screen position),但是我们没有办法控制偏移量

所以我们添加一个vector2类型的偏移量offset,默认值保持为0不变, 然后和default模式下的screen position节点相加,这样就可以控制了

​ image-20260605132202006

然后我们在为这个噪波效果添加偏移量

我们添加一个浮点类型的depth sample offset的默认范围(0~0.1)的slider滑块,这个赋值之所以比较低是因为这个代表的是整个屏幕的比例,偏移量超过屏幕10%会显得很难看

然后和噪声节点相乘并将输出值导入深度相交子节点为offset节点赋值

​ image-20260605133240281

image-20260605133911352

此时我们已经可以看到这个在边缘有泡沫了,但是目前的效果看起来很别扭

这是因为我们原先的是经过step节点将小于0的转换为0,大于1的转换为1,但是我们刚才增加的操作是在这个处理之前的,而水面以上的像素点深度交集为负值,导致这部分像素通过step时都会输出1产生泡沫

​ image-20260605134135580

所以还需要改动

​ image-20260605134844609

我们将divide的输出的负值值用negate取反乘以-1,然后接入另一个step节点来判断

然后将噪声节点再接入这个step节点来判断,并将两个step节点的输出值相乘,并重新和泡沫颜色节点相乘

​ image-20260605141243812

现在看起来就自然多了,泡沫会随着波浪的滚动方向飘

但是想必你也发现了,咱们的节点已经快变成蜘蛛网了,说明之前的操作必须要简化了,这里我们直接在devide和step节点之间添加一个absolute绝对值节点,顾名思义,就是我们初中学的那个化负为正的绝对值

仔细思考一下,刚才绕一圈子是为了什么? 就是因为有负值的出现导致边缘出现了大面积的泡沫,我们之前的操作相当于: 一个盒子里有很多红色(非负值)和蓝色(负值)的小球,我们只要红色的,于是我们用筛子分别筛出两种小球,然后把蓝色的小球染成红色,然后再把两种颜色的小球倒在一起,但是我们其实可以直接把所有小球都直接染成红色,也就是对所有值取绝对值

​ image-20260605142021724

这样效果仍然是一样的

​ image-20260605143205430

物体边缘发光效果

这个其实可以拓展为物体边缘的黑边或者白边这些效果,很多游戏都喜欢给角色或者物体添加一个描边

​ image-20260605151423006

像之前那样添加材质和基础色

​ image-20260605154303634

我们首先添加UV节点,UV节点用于网格的每个顶点的坐标,用于定义材质纹理贴图的位置,在unity中默认立方体的UV映射使立方体的每个面都能完整显示纹理 ,

text
12345678910
(0,1) ────── (1,1)
  ↑            ↑
  │            │
左边         右边
  │            │
(0,0) ────── (1,0)

UV的范围为: 
x: 0 → 1
y: 0 → 1

我们需要检测在x或y方向上接近0的UV坐标,(也就是x轴对应的u轴,以及y轴对应的v轴),这里分为两步

① 上半部分的smoothstep

所以我们添加一个名为edge threshold 默认值为 0.1 范围为0-0.5 的slider的浮点类型的节点来控制,0表示无发光效果,0.5表示从物体两侧各覆盖一半的长度

然后我们用Smoothstep 节点,将UV输入in插槽,将默认值为0的float节点和edge threshold节点的值设置为上下限 即 smoothstep(0, edge, uv.x)

UV节点输出的是四维向量(x,y,z,w),一般情况下只用x和y

从上方的 Smoothstep 节点的颜色可以看出检测的位置是左侧和底部的边缘,当UV值超过edge值的时候会输出为1,我们需要用One minus 节点来反转为0 ,因为原本的是左边下面黑,中间白,转换为左边下面白,中间黑

image-20260605221445722

这里我们只用x和y分量,我们用split节点将x和y分离,然后用add相加

② 下半部分的smoothstep

然后我们同样的用UV连接另一个Smoothstep节点 in输入口, 我们添加一个x值为1的float节点,然后让flaot和edge threshold节点用subtract节点相减,即 smoothstep(1-edge, 1, uv.x)

此时内部为黑,右侧和上侧为白,不需要反色

我这样解释可能更直观

​ image-20260606230919506

这里之所以要用split分离出来R和G 然后相加,是因为R和G通道输入分别输出的是红色和绿色,我们将二者相加变成1,那么其他区域就是0,也就是只剩下黑白色

这里复习一下几个节点的作用

① Smoothstep 节点-----平滑

端口作用
Edge1下限阈值(较小值)
Edge2上限阈值(较大值)
In当前输入值

输出规则

plain
123
In ≤ Edge1    →  输出 0
In ≥ Edge2    →  输出 1
Edge1 < In < Edge2  →  输出 0~1 之间的平滑过渡值

直观理解

想象一个渐变条:

plain

plain
123
0                    0.5                    1
|←──── 黑色 ────→|←──── 灰色过渡 ────→|←──── 白色 ────→|
      Edge1                          Edge2
  • Edge1 左边:纯黑(0)
  • Edge2 右边:纯白(1)
  • 中间:平滑的灰度过渡(S 形曲线,不是直线)

② One minus ------反向

Unity 的 One Minus 节点就是1 减去输入值:

plain
1
输出 = 1 - 输入
端口作用
In输入值(0~1 范围)
Out1 - In

没有 Edge,没有阈值,就是简单的反向。

直观理解

plain
123
输入 0  →  输出 1    (全黑变全白)
输入 0.5 →  输出 0.5  (中间不变)
输入 1  →  输出 0    (全白变全黑)

常见用法

场景说明
反相颜色白变黑,黑变白
反向遮罩原来显示的地方隐藏,隐藏的地方显示
补数运算配合其他节点做 1-x 的数学操作
反向透明度不透明变透明

③ Subtract ------相减

Unity 的 Subtract 节点 是 Shader Graph(着色器图)中的一个数学运算节点,用于执行减法运算

属性说明
输入 A被减数(Minuend)
输入 B减数(Subtrahend)
输出A - B 的结果

④ Saturate -----限制范围(0-1)

属性说明
输入任意数值(标量或向量)
输出将输入值钳制到 [0, 1] 范围

数学表达:output = clamp(input, 0, 1)

即:

  • 小于 0 的值 → 变为 0
  • 大于 1 的值 → 变为 1
  • 0 ~ 1 之间的值 → 保持不变

然后我们将左下角和右上角的值相加,因为角落会出现像素的总和大于一(这里我们打个比方,比如我们将两张纸条叠在一块,那么他们重叠的区域的纸条厚度一定大于一张纸条),为了避免交叉处的亮度和其他位置不一致,所以我们需要用saturate节点来限制值在(0,1)范围

​ image-20260607220728288

然后我们需要控制边框的颜色,这里添加一个Glowcolor颜色节点,设置为HDR,然后我们先用add节点预留一个接口,然后和glowcolor节点相乘并与基础色和纹理相加

然后将这个材质附加到物体上

​ image-20260607222811601

我们调整edge threshold的值就可以看到发光效果了

​ image-20260607223008473

刚才我们并没有添加与地面交叉的效果

这里我们复用之前的负责交叉逻辑的子图,用one minus取反(因为原本的是黑色阴影,这里变成白色),然后用saturate节点避免低于0的交集值,为了控制强度依旧添加power节点并连接浮点类型默认值为1 范围0.01-25的slider值 intersection power

​ image-20260607223823399

然后还记得吗,这个交叉效果必须是透明材质,否则无法显示出来

而且要注意的是这个效果只对不透明物体有效,对透明物体无效

​ image-20260607232149771

例如对普通不透明平面是这样

​ image-20260607232501774

对透明水体是这样,此时水体和箱子都无法互相检测,因为都是透明材质

​ image-20260607232615468