自由学习记录(74)

能否把 ComputeGrabScreenPos(o.pos) 改为 ComputeScreenPos(o.pos)

简而言之------不可以。这两个函数虽然类似,但用途不同,不能互换。


⚙️ 两者区别简明戳表

函数 用途 输出是否适合 GrabPass UV 是否处理 API 平台差异
ComputeScreenPos(clipPos) 用于一般屏幕采样(例如在屏幕空间贴图),生成 UV 坐标。 ❌ 不可靠 ✅ 处理 NDC 但不考虑 Flip
ComputeGrabScreenPos(clipPos) 专门用于 GrabPass 纹理采样,确保 UV 合适 GrabPass 输出格式。 ✅ 适合 GrabPass ✅ 包含垂直翻转与 API 差异处理

xxxx

总之,想要grabpass使用折射

要先转换成screen pos

采样的时候要/screenpos的w

屏幕空间像素坐标(Screen Space Pixels)

2. Shader 中 ComputeScreenPosComputeGrabScreenPos 输出的屏幕坐标

这是属于 图形管线内部的屏幕空间 ,它的 x, y, z, w 表示方法如下:

ComputeScreenPos(clipPos)
  • 输入是 裁剪空间clipPos

  • 输出 .xy 是齐次空间的屏幕坐标,需透视除以 .w[0,1]标准化 UV 坐标(对应整个屏幕区域)Unity Documentation+1Unity Documentation+1

  • .xy/w 后的范围才是归一化 UV,不再是像素值。

ComputeGrabScreenPos(clipPos)

屏幕坐标系,z依然代表深度值(继承于ndc),只是xy被拉长到1920x1080

z ------ 深度值

  • 继承自 NDC(Normalized Device Coordinates),表示顶点或片元的深度信息。

  • 这个值通常经过非线性压缩:在 D3D 中范围是 [0,1],而 OpenGL 是 [-1, +1],经过统一处理后在 URP 中被转换成 [0,1] 的深度缓冲格式。stackoverflow.comdocs.unity3d.com+6docs.unity3d.com+6docs.unity3d.com+6

  • z 可用于世界坐标重构、深度测试、屏幕空间效果等,不是屏幕平面的 z = 1

fixed3 refrCol = tex2D(_RefractionTex, i.scrPos.xy / i.scrPos.w).rgb;

这里如果按照一般的屏幕空间,i.scrPos.xy / i.scrPos.w是1920x1080的大坐标,不会在0到1之间采样,

所以说o.scrPos = ComputeGrabScreenPos(o.pos);里的屏幕空间只能算是修改压缩到0到1范围里的方便采样的"屏幕空间"

靠,服了,遭罪的公式,名字起的这么常见的,里面做了什么又不教你,就是硬套是吧,

grabpass硬套,rendertexture还好多了,至少可以当一张正常的图来用

cs 复制代码
	private Texture2D _GenerateProceduralTexture() {
		Texture2D proceduralTexture = new Texture2D(textureWidth, textureWidth);

		// The interval between circles
		float circleInterval = textureWidth / 4.0f;
		// The radius of circles
		float radius = textureWidth / 10.0f;
		// The blur factor
		float edgeBlur = 1.0f / blurFactor;

		for (int w = 0; w < textureWidth; w++) {
			for (int h = 0; h < textureWidth; h++) {
				// Initalize the pixel with background color
				Color pixel = backgroundColor;

				// Draw nine circles one by one
				for (int i = 0; i < 3; i++) {
					for (int j = 0; j < 3; j++) {
						// Compute the center of current circle
						Vector2 circleCenter = new Vector2(circleInterval * (i + 1), circleInterval * (j + 1));

						// Compute the distance between the pixel and the center
						float dist = Vector2.Distance(new Vector2(w, h), circleCenter) - radius;

						// Blur the edge of the circle
						Color color = _MixColor(circleColor, new Color(pixel.r, pixel.g, pixel.b, 0.0f), Mathf.SmoothStep(0f, 1.0f, dist * edgeBlur));

						// Mix the current color with the previous color
						pixel = _MixColor(pixel, color, color.a);
					}
				}

				proceduralTexture.SetPixel(w, h, pixel);
			}
		}

		proceduralTexture.Apply();

		return proceduralTexture;
	}

关键步骤说明:

1. 初始化纹理
cs 复制代码
Texture2D proceduralTexture = new Texture2D(textureWidth, textureWidth);

生成一个新的空纹理。

2. 准备参数
cs 复制代码
float circleInterval = textureWidth / 4.0f;
float radius = textureWidth / 10.0f;
float edgeBlur = 1.0f / blurFactor;
  • 圆的中心分布在 (1/4, 1/2, 3/4) 区域(因为是 (i+1)/4

  • 每个圆的半径是纹理宽度的 1/10。

  • edgeBlur 控制模糊的程度。

3. 遍历每一个像素 (w, h)
cs 复制代码
for (int w = 0; w < textureWidth; w++) {
	for (int h = 0; h < textureWidth; h++) {
		Color pixel = backgroundColor;

为每个像素准备默认色。

4. 遍历 3×3 的圆形位置(九宫格)
cs 复制代码
for (int i = 0; i < 3; i++) {
	for (int j = 0; j < 3; j++) {
		Vector2 circleCenter = new Vector2(circleInterval * (i + 1), circleInterval * (j + 1));
		float dist = Vector2.Distance(new Vector2(w, h), circleCenter) - radius;

计算每个圆心到当前像素的距离 dist,减去 radius 以判断像素是"在圆内"还是"在边缘"。

5. 进行边缘模糊与颜色混合
cs 复制代码
Color color = _MixColor(circleColor, new Color(pixel.r, pixel.g, pixel.b, 0.0f), Mathf.SmoothStep(0f, 1.0f, dist * edgeBlur));
pixel = _MixColor(pixel, color, color.a);
  • Mathf.SmoothStep 产生平滑插值控制系数(类似高斯边缘)

  • _MixColor 是作者自定义的线性插值函数,通常是:

    cs 复制代码
    Color _MixColor(Color a, Color b, float t) {
        return a * (1 - t) + b * t;
    }
  • 第二个参数的 alpha 是 0,表示只淡入圆的颜色边缘,不生硬覆盖背景。

6. 写入纹理像素
cs 复制代码
proceduralTexture.SetPixel(w, h, pixel);

✅ 最后一步

proceduralTexture.Apply();

这一行是必须的,它会真正把像素数据上传到 GPU

霜狼_may视频专辑-霜狼_may视频合集-哔哩哔哩视频

xxx

使用程序材质,之所以叫做程序材质,是因为里面用到的纹理是程序生成的纹理(substance designer就是)

程序材质和程序纹理都是sd里生成的

sqad的顶点结构,在模型空间下是竖直排列的

挂在摄像机上面,控制后处理

OnRenderImage(RenderTexture src, RenderTexture dest)不是 URP 或 SRP 专属的函数,它实际上是 Unity **内置渲染管线(Built-in Render Pipeline)**中的一项特有功能。

在 URP / HDRP 中是不能用 OnRenderImage 的!

原因:

SRP(Scriptable Render Pipeline)系统重写了整个渲染流程不再调用 OnRenderImage

Unity 官方在切换到 URP/HDRP 时明确废弃这类"管线钩子"方式,而转向更加结构化的 Render Feature / Pass injection 机制。

struct v2f {

float4 pos : SV_POSITION;

half2 uv[9] : TEXCOORD0;

};

确实是合法的 HLSL 写法,表示你想把 uv[0] ~ uv[8] 作为 9 个 half2 数据输出到片元着色器。但它是否能"正常运行"取决于 Shader Model(SM)版本 以及你绑定它的目标渲染管线(Built-in / URP / HDRP)和平台(PC / 移动)。

Sobel卷积计算中,需要 3×3 的纹理采样坐标偏移(UV offset)

我们是否可以将这些偏移 UV 全部提前在 vertex shader 中计算好并插值传入 fragment shader,而不在 fragment 中实时计算?


✅ 通常答案是:"可以,而且不会影响结果"

这是因为:

🧠 原因:++纹理 UV 插值是线性++ 的,纹理++采样坐标偏移也在 UV 空间中是线性++的

  • 顶点 shader 里计算的每个偏移 UV 是:

    uv + offset[i] * texelSize

  • 然后它们通过 varying 插值进入 fragment shader,UV 插值是线性的

  • 最终采样:

    tex2D(_MainTex, uv_offset[i])

由于卷积核采样点的坐标本来就是规则排列的(固定步长),所以在 fragment shader 插值得到的 uv_offset[i] 正好是你该采样的位置。

因此:

即使你没在每个 fragment 精确计算采样点偏移,而是通过插值从四个顶点计算过来的值,在大多数情况下它和 fragment 中现算的结果一致。

cs 复制代码
			fixed luminance(fixed4 color) {
				return  0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b; 
			}
			
			half Sobel(v2f i) {
				const half Gx[9] = {-1,  0,  1,
										-2,  0,  2,
										-1,  0,  1};
				const half Gy[9] = {-1, -2, -1,
										0,  0,  0,
										1,  2,  1};		
				
				half texColor;
				half edgeX = 0;
				half edgeY = 0;
				for (int it = 0; it < 9; it++) {
					texColor = luminance(tex2D(_MainTex, i.uv[it]));
					edgeX += texColor * Gx[it];
					edgeY += texColor * Gy[it];
				}
				
				half edge = 1 - abs(edgeX) - abs(edgeY);
				
				return edge;
			}

它从输入结构体 i 中取出 9 个预先计算好的纹理坐标 i.uv[0~8](分别对应 3x3 的卷积核中心和其八个邻居),并对这些位置执行 Sobel 算法来检测"边缘强度"。

fixed luminance(fixed4 color) {

return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b;

}

这段使用加权平均的方式计算 感知亮度,符合人眼对不同颜色敏感度(绿色最敏感),用于后续 Sobel 处理时不必再处理 RGB,而是用灰度强度。

half texColor;

half edgeX = 0;

half edgeY = 0;

for (int it = 0; it < 9; it++) {

texColor = luminance(tex2D(_MainTex, i.uv[it]));

edgeX += texColor * Gx[it];

edgeY += texColor * Gy[it];

}

这里对 3×3 的采样区域进行遍历,对应位置乘上 Gx/Gy 权重,累加为边缘梯度。

half edge = 1 - abs(edgeX) - abs(edgeY);

计算的是简单的 梯度强度和(L1范数)。

再用 1 - ... 得到"非边缘强度",所以边缘越强 → 值越小(接近 0),边缘越弱 → 值越接近 1。

你可以把这个值当作亮度返回,绘出边缘线。

"二维高斯核怎么能变成两个一维高斯核?为什么高斯模糊的结果还能一样?"

高斯核的可分性(Separable Property)

核心结论:

二维高斯核是可分离的,即一个二维的高斯函数可以拆解为两个一维的高斯函数的乘积!

cs 复制代码
using UnityEngine;
using System.Collections;

[ExecuteInEditMode]
[RequireComponent (typeof(Camera))]
public class PostEffectsBase : MonoBehaviour {

	// Called when start
	protected void CheckResources() {
		bool isSupported = CheckSupport();
		
		if (isSupported == false) {
			NotSupported();
		}
	}

	// Called in CheckResources to check support on this platform
	protected bool CheckSupport() {
		if (SystemInfo.supportsImageEffects == false || SystemInfo.supportsRenderTextures == false) {
			Debug.LogWarning("This platform does not support image effects or render textures.");
			return false;
		}
		
		return true;
	}

	// Called when the platform doesn't support this effect
	protected void NotSupported() {
		enabled = false;
	}
	
	protected void Start() {
		CheckResources();
	}

	// Called when need to create the material used by this effect
	protected Material CheckShaderAndCreateMaterial(Shader shader, Material material) {
		if (shader == null) {
			return null;
		}
		
		if (shader.isSupported && material && material.shader == shader)
			return material;
		
		if (!shader.isSupported) {
			return null;
		}
		else {
			material = new Material(shader);
			material.hideFlags = HideFlags.DontSave;
			if (material)
				return material;
			else 
				return null;
		}
	}
}

只是基类声明,把同用的后处理逻辑都写一起了,实际上对于图像的处理和这些没有关系

d都是子类里,onrenderimage函数,对图像进行处理,在子脚本里创建Material,接受shader用到Material上面,以及这里的函数,截取不透明||中间处理||半透明,然后利用上材质对这里进行处理,

cs 复制代码
	void OnRenderImage (RenderTexture src, RenderTexture dest) {
		if (material != null) {
			int rtW = src.width/downSample;
			int rtH = src.height/downSample;

			RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
			buffer0.filterMode = FilterMode.Bilinear;

			Graphics.Blit(src, buffer0);

			for (int i = 0; i < iterations; i++) {
				material.SetFloat("_BlurSize", 1.0f + i * blurSpread);

				RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);

				// Render the vertical pass
				Graphics.Blit(buffer0, buffer1, material, 0);

				RenderTexture.ReleaseTemporary(buffer0);
				buffer0 = buffer1;
				buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);

				// Render the horizontal pass
				Graphics.Blit(buffer0, buffer1, material, 1);

				RenderTexture.ReleaseTemporary(buffer0);
				buffer0 = buffer1;
			}

			Graphics.Blit(buffer0, dest);
			RenderTexture.ReleaseTemporary(buffer0);
		} else {
			Graphics.Blit(src, dest);
		}
	}

OnRenderImage(RenderTexture src, RenderTexture dest) 的调用时机:

是在当前摄像机完成整个一帧的所有渲染(包括不透明、透明、天空盒等所有 passes)之后,马上被调用,用于执行"整屏后处理效果"。

所以 不只是"不透明 pass"之后,而是:

阶段 是否已完成
不透明物体 ✅ 是
天空盒 ✅ 是
半透明/透明物体 ✅ 是
所有图像绘制完成 ✅ 是
OnRenderImage 调用 🔜 接下来

Unity 5(估计都这样) 中 OnRenderImage()引擎级别在摄像机渲染结束后触发的回调:

  • 它不在 C# 控制的 Render Loop 中;

  • 它不是 CommandBuffer;

  • 它没有参数允许你选择"挂载在哪一阶段"。

想"完全控制渲染顺序"的方式:

唯一真正可以"自由插入任意阶段"的机制是:

  • 使用 SRP(Scriptable Render Pipeline)

  • 比如 URP / HDRP 自定义 RenderFeatureRenderPass

但 Unity 5 并不支持 SRP(那是 Unity 2018.1+ 才引入的)。

cs 复制代码
	void OnRenderImage(RenderTexture src, RenderTexture dest) {
		if (material != null) {
			int rtW = src.width;
			int rtH = src.height;
			RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0);

			// Render the vertical pass
			Graphics.Blit(src, buffer, material, 0);
			// Render the horizontal pass
			Graphics.Blit(buffer, dest, material, 1);

			RenderTexture.ReleaseTemporary(buffer);
		} else {
			Graphics.Blit(src, dest);
		}
	} 

典型的 双 pass 高斯模糊(Gaussian Blur) 实现,使用 分离的垂直(vertical)和水平(horizontal)模糊 Pass

一个 shader 的两个 Pass(第 0 个和第 1 个),对图像先进行垂直方向的模糊,再进行水平方向的模糊,实现高性能、高质量的模糊效果。

void OnRenderImage(RenderTexture src, RenderTexture dest)

这个函数在一帧渲染完后被 Unity 自动调用,src 是当前帧图像,dest 是输出目标(最终屏幕或后处理链的下一个 RenderTexture)。

RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0);

创建一个临时 RT(buffer)用于中间结果的存储,尺寸与屏幕一致。

Graphics.Blit(src, buffer, material, 0);

将源图像 src 通过 material 的 第 0 个 Pass 处理,结果写入 buffer

这个 Pass 通常是对 Y 方向(垂直方向) 的高斯模糊

Graphics.Blit(buffer, dest, material, 1);

将上一步结果 buffer 再通过 material 的 第 1 个 Pass 处理,结果写入 dest

这个 Pass 对 X 方向(水平方向) 进行模糊

RenderTexture.ReleaseTemporary(buffer);

释放 buffer,避免内存泄漏。

else {

Graphics.Blit(src, dest);

}

如果没有指定 material,则直接把原图 src 输出到 dest,不会有任何后处理。

为什么要用"两次一维模糊"?

高斯核的可分离性:

buffer.filterMode = FilterMode.Bilinear;

是设置这个临时 RenderTexture(buffer)的 采样过滤模式。这是非常关键的优化手段,尤其在**图像缩小(DownSample)**的后处理流程中。

✅ 它的作用是:

让 在模糊 Pass 中采样这个缩小图像时更加平滑,避免锯齿或块状感。

即:采样过程中采用 双线性插值(Bilinear Interpolation),而不是最近点采样(Point)。

模式 含义 用途场景(常见)
Point 最近点采样(Nearest Neighbor) 像素风格、无模糊
Bilinear 双线性插值(插值周围 4 像素) 图像缩放、模糊处理
Trilinear 三线性插值(含 Mipmap 层级) 3D 模型贴图、含 Mipmap 的情况

在这个模糊操作中:

你把 src 从 1920x1080 缩放到比如 480x270,再进行模糊处理。

如果你使用:

  • Point:会导致缩小时像素直接跳变,出现锯齿;

  • Bilinear:会在采样时对周围像素做插值,让模糊更自然。

CGINCLUDE 抽象出通用函数/结构,两个 Pass 共享

_BlurSize: 模糊强度倍率,控制采样偏移距离

struct v2f {

float4 pos : SV_POSITION;

half2 uv[5] : TEXCOORD0;

};

一次采样 5 个纹理坐标:

uv[0]: 中心

uv[1~4]: 上下或左右两个方向的 ±1、±2 像素偏移

Pass 流程(在 C# 中调用):

  1. Pass 1 垂直方向模糊
    Graphics.Blit(src, buffer, material, 0);

  2. Pass 2 水平方向模糊
    Graphics.Blit(buffer, dest, material, 1);

通过两次一维模糊,效果等价于一次二维高斯核卷积,但性能提升巨大(从 O(n²) 降为 O(2n))。

使用场景

  • 模糊背景(UI、高光)

  • 模拟散焦/景深

  • 后处理 bloom(结合亮度提取)

为什么只用了 5 个采样点?

因为每次只在 一个方向上进行卷积

  • 1 个中心点 + 上下或左右方向上各 2 个点(±1、±2 像素偏移)

  • 共 5 个点:[uv0, uv±1, uv±2]

这是对高斯分布进行的近似采样,采样权重对应的是:

weight[0] = 0.4026 // center

weight[1] = 0.2442 // ±1

weight[2] = 0.0545 // ±2

合起来能很好地逼近二维高斯核:

Pass 1:纵向模糊(中心 ±1y ±2y)

Pass 2:横向模糊(中心 ±1x ±2x)

先竖直后水平,是这样的

如果是5x5,那也是沿着这一条直线再上下再加一格

如果只是十字,那会漏了边角,这里的技巧就是遍历的时候,自己的边角会被别的像素算上

其他的模糊结果然后参与自己的模糊计算,所以最后 的表现结果就会是正确的

相关推荐
西岸行者6 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意7 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码7 天前
嵌入式学习路线
学习
毛小茛7 天前
计算机系统概论——校验码
学习
babe小鑫7 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms7 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下7 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。7 天前
2026.2.25监控学习
学习
im_AMBER7 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J7 天前
从“Hello World“ 开始 C++
c语言·c++·学习