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

现实中的物体如果发生相交就会在两者的接触面会有阴影,例如你的手机放在桌面上可以看到手机和桌面的黑色阴影接触线,这个在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 矩阵只能做旋转和缩放无法做平移
$$
\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:
$$
\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)

可以平移:

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

不能平移(方向不变):

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

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


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

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

1
2
3
4
5
裁剪空间输出: (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 从深度纹理中读取当前屏幕位置记录的深度值,表示:

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

参数 全称含义 数值范围 核心特点
Eye Eye-space depth 非线性,近处密集、远处稀疏 相机空间的真实深度值,非线性分布
Linear01 Linear 0-1 depth [0, 1] 线性映射到 [0,1],0=近裁剪面,1=远裁剪面
Raw Raw 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

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

1
输出 = 1 - 输入

数学本质

输入 输出
0 1
0.25 0.75
0.5 0.5
0.75 0.25
1 0

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

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

数学公式

plain

1
2
Lerp(A, B, T) = A + (B - A) × T
= A × (1 - T) + B × T
T 值 结果
0 完全 A
0.25 75% A + 25% B
0.5 A 和 B 各一半
0.75 25% A + 75% B
1 完全 B

三个端口

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

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

数学公式

plain

1
2
3
4
5
Saturate(x) = clamp(x, 0, 1)

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

输入输出对照

输入 输出
-0.5 0
0 0
0.3 0.3
0.5 0.5
0.8 0.8
1 1
1.5 1
2 1

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

image-20260603162438341

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

image-20260603195615167

然后露出来很多

image-20260603195644722

这样就很直观了