自由学习记录(72)
你的问题已经不是:
“我该怎么写一个 shader 实现 RimLight”
而变成:
🧠 “URP 的 SRP 框架里,这一块由谁管?我还用自己写吗?”
所以你在学习 URP 时真正要解决的几个核心问题是:
模块 | 问题 |
---|---|
🧩 光照获取 | GetMainLight() 怎么拿的? MainLight 和 AdditionalLight 结构怎么查? |
🔧 变换函数 | TransformObjectToHClip() 等 SRP 内建函数在哪?是否等价于 Built-in 中的 MVP 乘法? |
🧱 光照模型替换 | 我怎么写自定义光照?能不能复用之前写的 Lambert?怎么让它插入到 pipeline 的正确位置? |
🧵 Pass 编排 | 为什么 Unlit 只写一个 Pass 就够?为什么 Lit 是靠 Include 搭起来的?我要重写整个还是只替换? |
📁 include 系统 | #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/*.hlsl" 到底每个文件负责什么? |
🕳️ Feature vs Shader | 有些效果(如 Outline、后处理)在 URP 里居然是放到 Renderer Feature 而不是 Shader 本体里?那该放哪? |
理解 Render Feature 插入的时间点
-
URP 把很多原本“Shader 本体内部”负责的事情,交给了“Frame Pass Queue”来调度。
-
你得熟悉
ScriptableRendererFeature
,RenderObjects
,Blit
,DrawObjects
等接口。 -
一些效果甚至不该写在 Shader,而该写成 Feature。
Unity 官方的教学逻辑(教学设计理念)
Unity 并不希望初学者“从裸写 URP Shader 起步”,所以它:
-
提供的是一层层包裹的功能解构(先用,再改,再深入);
-
鼓励你通过:
✅ 改官方模板 + 🔍 查看 Shader 源码 + 🧩 插入 Renderer Feature
来渐进式掌握 SRP 渲染管线;
不像传统 OpenGL 或书本教材从“画三角形”讲起,而是让你:
从工程角度理解模块分工,再逐步控制各层逻辑。
如果你想“从官方角度完整学 URP Shader”,推荐顺序如下:
阶段 | 资源 | 学习重点 |
---|---|---|
Step 1 | Unity Docs:URP Shader Overview | 理解 URP Shader 结构组成(HLSLPROGRAM、Pass、Include) |
Step 2 | 官方 GitHub 示例工程 | 运行 + 阅读实际 Shader,理解光照、Shadow、Rim 等功能怎么实现 |
Step 3 | 打开 ShaderLibrary | 跟踪 GetMainLight() 等函数底层原理(Light struct, LightData) |
Step 4 | 创建 Renderer Feature | 学习怎么挂自定义 Pass 到 Frame 中(后处理、深度描边) |
Step 5 | 自己写一个基于 URP 的效果(比如 Rim + ShadowRamp) | 全流程闭环一遍 |
https://github.com/Unity-Technologies/UniversalRenderingExamples
-
Assets/Shaders/
文件夹:-
UnlitMinimal
是最小的 URP Shader 示例; -
CustomLighting
里是完整光照结构; -
RimLighting
,ShadowRamp
,BlinnPhongLighting
这些你一定要看;
-
-
场景中点击对应模型,看 Inspector 中材质绑定的是哪个 Shader;
-
点进去 Shader 文件对照源码 + 实际效果读。
打开本地 URP ShaderLibrary 分析函数来源
推荐文件:
-
Lighting.hlsl
→ 核心光照结构体Light
,GetMainLight()
,AdditionalLights
; -
Core.hlsl
→ MVP 变换、半兰伯特、正交空间函数; -
Shadows.hlsl
→ ShadowCaster、shadow bias 设置。
学习并创建 Renderer Feature
Renderer Feature 是 URP 中插入自定义 Pass(例如后处理、轮廓描边、深度图处理)的机制。
下载的示例项目中:
Assets/RendererFeatures/OutlineFeature.cs
Assets/RendererFeatures/CustomRenderPassFeature.cs
💡 跟踪这些文件,看它们怎么注册自定义 Pass、怎么在帧执行流程中注入、怎么调通 Shader。
Lighting.hlsl
中重构自己的光照模型
写一个 Renderer Feature 给它后处理一个描边或者辉光
“Unity 领域的 StackOverflow + 论坛” 混合体
有技术深度,也有教程型的讨论串。
-
收藏一些你常用的关键词(比如“Shader Graph dissolve”、“URP lightprobe setup”)
-
📑 点进别人提问看看有哪些回答,从回答里学写法与工具流程
-
🗂 如果你准备做作品集,里面也能搜到别人对特效/动画等项目型问题的处理方式
【PC High】包含高成本渲染特性(需要独立显卡 Dedicated GPU)
可能包括:
-
实时阴影(Real-time Shadows)
-
高分辨率光照贴图(Lightmap)
-
多 Pass Shader(多个 Pass 同时运行)
-
后处理特效(Bloom、Motion Blur、Depth of Field)
-
高质量抗锯齿(MSAA、TAA)
-
高材质分辨率(如 4K PBR 贴图)
🎯 这些特性对 显卡频宽、GPU 核心数、VRAM 有较高要求,集显常会卡顿、掉帧甚至崩溃。
【PC Low】配置相对简化,适合集显或低端机型
一般会:
-
关闭后处理(或只留最基础的)
-
使用低分辨率光照贴图或完全禁用
-
使用 LOD 较低的模型
-
简化 Shader 中的流程,比如关闭法线贴图或镜面反射
-
渲染路径选用 Forward 而非 Deferred(减少 Pass)
📉 用于节省 GPU 运算负担,提高流畅度,哪怕牺牲一点视觉质量。
🎓 为什么教程要这样提示你?
因为 Unity 的 URP Sample 项目中很多场景(如 The Garden、The Oasis)用到了:
-
多光源照明
-
复杂 Shader 和动态特效
-
高质量后处理管线
这些在集显上跑会掉帧,甚至卡死,而作者希望你能流畅体验教程内容,避免误以为“Unity 卡”。
https://docs.unity3d.com/2022.3/Documentation/Manual/class-QualitySettings.html
每个项目的渲染需求不同,无法靠一个固定渲染管线满足全部平台、风格、性能目标,因此需要可定制的 SRP。
【直播回放】从0写一个SRP_哔哩哔哩_bilibili
可以尝试在这里复制名字,然后在package Manager里找到url导入的,粘贴上去
RenderPipeline
是 SRP 的基类
你必须继承它,并重写唯一关键方法:
protected override void Render(ScriptableRenderContext context, Camera[] cameras)
foreach (var camera in cameras)
{// 1. 设置相机属性(matrix、clear flags 等)context.SetupCameraProperties(camera);// 2. 创建渲染命令缓冲(CommandBuffer)// 3. 处理光照、Culling(剔除不可见物体)// 4. 绘制场景对象(DrawRenderers)// 5. 后处理(如果你实现了)// 6. 提交绘制指令context.Submit();
}
不仅立方体没有,连scene里面的gizmo也没有,只draw skybox
此时旋转相机都不会有用,连相机的位置都需要自己设置
和这种写法一致(在最新的一些版本里加上去的,就是可以把类型直接写在参数里面了)
(新的c#语法里有)
裁剪的是cull,这个是unity依然帮忙直接做了,一般也没有更好的方法了,
out
的关键特性:你不需要在调用前 new 它,因为:
👉 方法内部负责 new 并赋值,调用者只需要声明变量即可。
你只是在“声明变量”,不需要初始化(new),因为 out
保证:
🔸 Unity 的这个方法会在内部执行
parameters = new ScriptableCullingParameters(...);
🔸 并填好其中的字段(如视锥体、遮挡剔除选项、LOD 层级等)
ref ds
, ref fs
:
-
你在调用前就必须创建好 ds 和 fs 实例
-
Unity 会通过引用去读取它们的内容,甚至可能修改你传进去的对象(尽管通常不修改)
如果 ScriptableCullingParameters
是引用类型,是不是就不需要 out
了?
答案是:它是值类型(struct),所以你必须用 out
!
Unity 的很多核心渲染结构(包括 ScriptableCullingParameters
, CullingResults
, DrawingSettings
, FilteringSettings
等)都是 struct
:
xxxx
写完之后,还是没有看到效果
而原因是---场景里的这些物体用的依然是standard shader,
srp里不支持内置管线的shader ,但是有一种除外,就是unlit shader
因为unlit shader不受光照影响
-----
还不够,还需要别的
这样才最后可以看见了
类型 | 作用 |
---|---|
DrawingSettings | 💡 定义如何绘制物体(用哪个 Shader Pass、排序、批处理方式) |
FilteringSettings | 🧹 定义要绘制哪些物体(基于 LayerMask、Queue、Shader tag 等) |
index=0
是 主 Pass,最优先尝试匹配的 Shader Pass 名
index=1/2/...
是 备用 Pass,如果前面的找不到,再尝试它们
它们组成了一个 “Shader Pass 匹配表”
为什么是 "SRPDefaultUnlit"
?
-
这是 Unity 内置 SRP(包括 URP)使用的 Unlit Shader Tag
-
如果你写自定义 Shader 时没有设置
Name
,可能不会被匹配到
ds.SetShaderPassName(0, new ShaderTagId("MyForwardLit"));
ds.SetShaderPassName(1, new ShaderTagId("SRPDefaultUnlit"));
Unity 在执行 context.DrawRenderers() 时,会做如下查找:对每个渲染器的材质所使用的 Shader,去里面找:
有没有 Pass { Tags {... "LightMode" = "MyForwardLit" ...} ...} → ✅ 用这个
没有 → 有没有 Pass { Tags {... "LightMode" = "SRPDefaultUnlit" ...} ...} → ✅ 用这个
都没有 → ❌ 不渲染
FilteringSettings核心作用:
你告诉 SRP:
“我这次
DrawRenderers()
调用,不限制任何 Render Queue 范围,什么都可以画。”
RenderQueueRange
是对材质 RenderQueue
的一个区间过滤器。
参数 | 描述 |
---|---|
RenderQueueRange.opaque | 画不透明物体(默认 queue 0–2500) |
RenderQueueRange.transparent | 画透明物体(queue 2501–5000) |
RenderQueueRange.all | 全都包含,不做筛选 |
用途 | ShaderTagId | FilteringSettings |
---|---|---|
画不透明物体 | "SRPDefaultUnlit" | RenderQueueRange.opaque |
画透明物体 | "SRPDefaultUnlit" | RenderQueueRange.transparent |
画阴影 | "ShadowCaster" | 任意 Queue |
自定义特殊 Pass | "MyCustomPass" | 自定义 Layer/Queue 范围 |
DrawingSettings
, FilteringSettings
, ScriptableRenderContext
, ShaderTagId
等,全都是 Unity 官方 Scriptable Render Pipeline (SRP) 的核心底层类,统称为:
🔧 UnityEngine.Rendering 命名空间下的 SRP API
是 Unity 提供的底层可编程渲染接口集合,你可以完全重写 Unity 的渲染行为。
https://docs.unity3d.com/ScriptReference/Rendering.ScriptableRenderContext.html
都可以在官方文档的一个一个查
https://docs.unity3d.com/Packages/com.unity.render-pipelines.core@17.3/manual/index.html
Unity 官方自定义 SRP 的核心文档入口
xxxx
这里的文字绑定到相机上面,和相机一起移动,然后在后面自己额外增加关键帧移动,到中间再,,,,
不行,他这里一开始是和背景同一位置的,后面才冲回了视野的中心位置,不适合设置父对象
这里轻微的调整了一下,导致了和父对象的一点轻微差距
两个节点(漫射 BSDF、透明 BSDF)在渲染模型中可以被视为同级的 BRDF 模型
在 Forward 渲染中,一个 Pass 通常只负责处理一种光源影响(比如主光 / 额外点光),
多光源的支持依赖于多个 Pass 的叠加处理,或者特殊的聚合方法。
在 Forward 渲染路径中:
类型 | 描述 |
---|---|
ForwardBase Pass | 处理主方向光(Directional Light)+ 环境光(Ambient) |
ForwardAdd Pass | 每个处理一个额外的光源(Point / Spot Light) |
ShadowCaster Pass | 处理阴影生成 |
Meta / Deferred / DepthOnly | 用于 GI / 探针 / 深度图等特定目的 |
Pass
{
Tags { "LightMode" = "ForwardBase" } // 处理主光 + ambient
}
Pass
{
Tags { "LightMode" = "ForwardAdd" } // 每个 point/spot light 分别加一个 pass
Blend One One // 累加混合(Additive)
}
Unity 会自动为每个额外光源调用一次 ForwardAdd pass,并将光源数据注入 Shader 中(如 _LightColor0, _WorldSpaceLightPos0)
延迟渲染的 Lighting Pass 并不是在材质 Shader 中处理的,而是在后期一个全屏 Shader 中,你可以循环多个光源、逐像素叠加它们的贡献,不需要为每个光源重绘物体!
✔ 在延迟渲染中,你的材质 Shader(GBuffer Pass)只需写一个 Pass,且完全不负责光照逻辑。
✔ 所有光源的计算都发生在 Unity 内部的一个 Lighting Pass 中(不是你写的 Shader),
✔ 所以:你不需要在材质 Shader 中访问光源数据,也不需要写多个 Pass 来支持多个光源。
如果你想打破这套结构,你可以做的高级玩法是:
✅ 自己重写 Lighting Pass(SRP 中的
DeferredLights.hlsl
+ C# Pass 组织逻辑)
这样你就可以:
-
自定义光源类型(如区块状灯、颜色噪声灯)
-
加入多 BRDF 组合模型(如 anisotropic、clearcoat)
-
用其他曲面响应模型替代标准 PBR(如 cartoon 光照)
一个能在 Unity 默认 Deferred 渲染路径下生效的最基本 Shader,它:
-
✅ 只写一次 Pass
-
✅ 输出 GBuffer 所需的基础材质信息(Albedo、Normal、Metallic、Smoothness、Emission)
-
❌ 不参与光照计算(Deferred Lighting Pass 会处理)
Shader "Custom/DeferredMinimal"
{Properties{_MainTex ("Base Color", 2D) = "white" {}_Color ("Tint", Color) = (1,1,1,1)_Metallic ("Metallic", Range(0,1)) = 0.0_Smoothness ("Smoothness", Range(0,1)) = 0.5_EmissionColor ("Emission", Color) = (0,0,0)}SubShader{Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalRenderPipeline" }LOD 100Pass{Name "GBuffer"Tags { "LightMode" = "Deferred" }HLSLPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct appdata{float4 vertex : POSITION;float3 normal : NORMAL;float2 uv : TEXCOORD0;};struct v2f{float2 uv : TEXCOORD0;float3 normalWS : TEXCOORD1;float4 pos : SV_POSITION;};sampler2D _MainTex;float4 _MainTex_ST;float4 _Color;float _Metallic;float _Smoothness;float4 _EmissionColor;v2f vert (appdata v){v2f o;o.pos = UnityObjectToClipPos(v.vertex);o.uv = TRANSFORM_TEX(v.uv, _MainTex);o.normalWS = UnityObjectToWorldNormal(v.normal);return o;}struct GBufferOutput{float4 gbuffer0 : SV_Target0; // Albedo.rgb, Metallicfloat4 gbuffer1 : SV_Target1; // Normal.xy, Smoothness, Occlusionfloat4 gbuffer2 : SV_Target2; // Emission.rgbfloat4 gbuffer3 : SV_Target3; // Reserved (optional)};GBufferOutput frag (v2f i){GBufferOutput o;float3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;float3 normalWS = normalize(i.normalWS);float metallic = _Metallic;float smoothness = _Smoothness;float occlusion = 1.0;o.gbuffer0 = float4(albedo, metallic);o.gbuffer1 = float4(normalWS.xy, smoothness, occlusion);o.gbuffer2 = float4(_EmissionColor.rgb, 1.0);o.gbuffer3 = 0; // Reserved (deferred fog, etc)return o;}ENDHLSL}}FallBack "Diffuse"
}
如果是在内置管线,那就差别很多了(主要在光照模型变了,所以需要的输出标准Gbuffer的格式变了)
在 Built-in Deferred 渲染中,你的 Shader 必须满足以下要求:
要求 | 描述 |
---|---|
包含 Tags { "LightMode" = "Deferred" } 的 Pass | 用于写入 GBuffer,Unity 的内置 Deferred 渲染器会调用它 |
输出标准 GBuffer 格式(Albedo, Normal, Specular) | Unity 内部定义格式 |
或者直接使用 #pragma surface + deferred 修饰符 | ✅ 推荐方式,最简单稳定 |
就比如直接使用surface shader作为光照计算基础,
写
Shader "Custom/BuiltinDeferredMinimal"
{Properties{_MainTex ("Albedo", 2D) = "white" {}_Color ("Color", Color) = (1,1,1,1)_Glossiness ("Smoothness", Range(0,1)) = 0.5_Metallic ("Metallic", Range(0,1)) = 0.0}SubShader{Tags { "RenderType"="Opaque" }LOD 200CGPROGRAM// Surface Shader 开启延迟渲染#pragma surface surf Standard fullforwardshadows deferredsampler2D _MainTex;fixed4 _Color;half _Glossiness;half _Metallic;struct Input{float2 uv_MainTex;};void surf (Input IN, inout SurfaceOutputStandard o){fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;o.Albedo = c.rgb;o.Metallic = _Metallic;o.Smoothness = _Glossiness;o.Alpha = 1;}ENDCG}FallBack "Diffuse"
}
元素 | 含义 |
---|---|
#pragma surface surf Standard deferred | 表明使用延迟路径生成 GBuffer(Unity 内部处理) |
SurfaceOutputStandard | Unity 内部的数据结构,自动输出 GBuffer |
不需要手动写 GBuffer | Unity 自动处理你返回的 Albedo、Normal、Specular 等分量 |
✅ 光照将统一在 Unity Lighting Pass 中处理 | 包括所有光源影响(延迟的最大优势) |
⚠️ 如果你想写自定义 GBuffer 的 Vertex/Fragment Shader
那就麻烦多了,必须:
-
使用
Pass { Tags { "LightMode"="Deferred" } }
-
自己输出标准 GBuffer 结构到
SV_Target0~2
-
遵循 Unity 的内置 GBuffer 格式:
Render Target | 内容 |
---|---|
SV_Target0 | Albedo.rgb + Occlusion |
SV_Target1 | Specular + Roughness |
SV_Target2 | Normal + Emission(可选) |
这适合高手使用,Unity 并不推荐在内置管线中这么做。
“如果我也想像 URP 一样,在内置管线用 V/F shader 输出 GBuffer,会差很多吗?”
你无法更改 GBuffer 结构(必须符合 Unity 的写死格式)
Unity 内置延迟渲染的 GBuffer 结构如下(固定):
Render Target | 内容(通道布局) |
---|---|
SV_Target0 | Albedo.rgb + Occlusion (a) |
SV_Target1 | Specular.rgb + Roughness (a) |
SV_Target2 | Normal.xyz + unused |
SV_Target3 | reserved (only HDRP uses) |
而在 URP 中,你可以完全自定义这 4 个 GBuffer 的格式,比如你可以让 GBuffer2
专门放 clearcoat 或 anisotropy。
你不能重写 Lighting Pass 的任何逻辑
Unity Built-in Deferred Lighting 是引擎内部的 C++ 层代码,你无法修改:
-
Blinn-Phong vs Cook-Torrance
-
灯光衰减函数
-
阴影查找方式
-
IBL/反射探针混合规则
而在 URP 中,这一切都可以在 DeferredLighting.hlsl
或 Lighting.hlsl
里修改。
写法不算差很多,但“灵活性差很多”
如果要写一份
Shader "Custom/BuiltinDeferredVF"
{Properties{_MainTex ("Albedo", 2D) = "white" {}_Color ("Color Tint", Color) = (1,1,1,1)_SpecColor ("Specular Color", Color) = (0.04, 0.04, 0.04, 1)_Glossiness ("Smoothness", Range(0,1)) = 0.5_Occlusion ("Occlusion", Range(0,1)) = 1.0}SubShader{Tags { "RenderType" = "Opaque" }LOD 100Pass{Name "Deferred"Tags { "LightMode" = "Deferred" }CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct appdata{float4 vertex : POSITION;float3 normal : NORMAL;float2 uv : TEXCOORD0;};struct v2f{float2 uv : TEXCOORD0;float3 normalWS : TEXCOORD1;float4 pos : SV_POSITION;};sampler2D _MainTex;float4 _Color;float4 _SpecColor;float _Glossiness;float _Occlusion;v2f vert (appdata v){v2f o;o.pos = UnityObjectToClipPos(v.vertex);o.uv = v.uv;o.normalWS = UnityObjectToWorldNormal(v.normal);return o;}struct FragOutput{float4 rt0 : SV_Target0; // Albedo.rgb + Occlusionfloat4 rt1 : SV_Target1; // Specular.rgb + Smoothnessfloat4 rt2 : SV_Target2; // Normal.xyz};FragOutput frag(v2f i){FragOutput o;float3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;float3 normal = normalize(i.normalWS);o.rt0 = float4(albedo, _Occlusion);o.rt1 = float4(_SpecColor.rgb, _Glossiness);o.rt2 = float4(normal, 1.0); // 第四分量通常未用return o;}ENDCG}}FallBack "Diffuse"
}