自由学习记录(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,那也是沿着这一条直线再上下再加一格

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

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

相关推荐
Brookty1 小时前
Java线程安全与中断机制详解
java·开发语言·后端·学习·java-ee
飞速移动的代码菌1 小时前
【DataWhale】快乐学习大模型 | 202507,Task08笔记
笔记·学习
不在了情绪1 小时前
[ The Missing Semester of Your CS Education ] 学习笔记 Vim篇
笔记·学习·vim
试着1 小时前
零基础学习性能测试第五章:JVM性能分析与调优-JVM概念,java程序运行原理
java·jvm·学习·零基础·性能测试
遇见尚硅谷3 小时前
C语言:20250728学习(指针)
c语言·开发语言·数据结构·c++·笔记·学习·算法
Aronup3 小时前
NLP学习开始01-线性回归
学习·自然语言处理·线性回归
墨迹的陌离5 小时前
【Linux】重生之从零开始学习运维之Mysql
linux·运维·服务器·数据库·学习·mysql
幻风_huanfeng5 小时前
学习人工智能所需知识体系及路径详解
人工智能·学习
试着6 小时前
零基础学习性能测试第六章:性能难点-Jmeter文件上传场景压测
学习·jmeter·零基础·性能测试
小一亿6 小时前
【0基础PS】PS工具详解--选择工具--对象选择工具
学习·平面·adobe·信息可视化·传媒·photoshop