自由学习记录(73)
着色器入门6:前向渲染与延迟渲染_哔哩哔哩_bilibili
URP管线下,运行时是否可以动态切换?
🟡 可以,但不是“自动切换”,你要操作 URP 的渲染管线设置。
方法 1(推荐):
-
准备两个 URP Renderer(一个设为 Forward,一个为 Deferred)
-
在相机中切换渲染器:
【TA进阶】Unity中的延迟渲染技术_哔哩哔哩_bilibili
2023版本的unity的新的forward+,处理了deffered render下的半透明物体的问题
要么就是自定义处理
Avatar
—— Unity 中绑定骨骼系统到动画控制器(Animator)用的抽象对象
是模型的“人物骨架描述”,不是查看骨骼的工具。
Strip Bones 是干什么的?
你截图中的 ✔ Strip Bones
表示:
Unity 在导入时会剥离掉未被权重影响的骨骼(优化用)
-
如果你想完整看到所有骨骼,建议先取消勾选 Strip Bones 再 Apply。
-
否则没用的骨骼会被优化掉,看不见。
https://docs.unity3d.com/6000.1/Documentation/Manual/FBXImporter-Rig.html
Generic 模式就是完全使用 FBX 模型里自带的骨骼结构,不做任何人形重定向。
-
不做“humanoid avatar mapping”
-
FBX 里怎么绑的骨,就怎么执行动画
-
所以适合非人类结构的角色,比如:怪物、机械、MMD 模型、定制人物
Unity Shader 105 - 多光源 和 Shader Pass_哔哩哔哩_bilibili
Unity多Pass的 动态合批 规则_哔哩哔哩_bilibili
关于光照的什么都没写进shader(即没有用任何光源变量、函数、宏),Unity 就不会处理任何光照。
决定光照是否“有效”的关键不是光源存在与否,而是 Shader 是否“使用”了光照信息。
如果你只保留了这个 Shader 的 ForwardBase Pass(不写 ForwardAdd Pass),而场景中存在 4 个点光源且都设置为了
Important
,那么会发生什么?
Unity 如何处理这些光源?
Pass 名称 | 是否会被 Unity 调用 | 渲染行为 |
---|---|---|
ForwardBase | ✅ 会调用(你写了) | 只处理主光源(最亮的那个点光源) |
ForwardAdd | ❌ 不会调用(你没写) | 其他 3 个光源无处计算 → 被跳过 |
如果你写了 Add Pass,但屏蔽了 #pragma
补充一点:
即使你写了 Add Pass,但没有 #pragma multi_compile_fwdadd
,Unity 也不会启用那些光照变体。一定要两者都写:
Tags { "LightMode" = "ForwardAdd" }
#pragma multi_compile_fwdadd
否则 Unity 编译器不会认为你支持 Add Pass。
建议小结
你想要的行为 | 需要的设置 |
---|---|
只要一个光源、没有多 Pass | ✅ 只写 ForwardBase 即可,场景中最多保留一个主光源 |
要多个逐像素点光源生效 | ✳ 写 ForwardAdd Pass + 设置 #pragma multi_compile_fwdadd |
想忽略点光源影响 | ✅ 设置它们为 Not Important ,并移除所有光照路径 |
如果我不写 Add Pass,那能不能在 Base Pass 中“直接处理所有 4 个点光源”?
情况一:这些光源被 Unity 判定为“逐顶点光”
例如:
-
RenderMode =
Not Important
-
或 RenderMode =
Auto
且光源离模型远、光照弱
那么:
👉 Unity 会把这些光源数据打包为顶点光数据,通过 内置的 unity_LightColor
, unity_4LightPosX0
, 等数组传入,你可以在 vertex shader 里循环计算多个光源贡献,然后在 fragment shader 里插值输出。
这种就是你说的 “Base Pass 里把多个光源都加进来算”。
示例代码片段(Unity 内置支持 4 个点光)
// 顶点光结构(Unity 内部默认 4 个)
uniform float4 unity_LightColor[4];
uniform float4 unity_LightPosition[4];// vertex shader 中:
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;fixed3 accumulated = 0;for (int i = 0; i < 4; ++i) {float3 lightDir = normalize(unity_LightPosition[i].xyz - worldPos);float NdotL = max(0, dot(worldNormal, lightDir));accumulated += unity_LightColor[i].rgb * NdotL;
}
❌ 情况二:光源是 Important
→ Unity 判定为逐像素光
如果 4 个点光源都设置成 Important
,Unity 会:
-
选一个最亮的塞进 ForwardBase;
-
剩下的 必须要有 ForwardAdd Pass 才能处理;
-
❗它不会自动把剩下的合并进 ForwardBase。
所以:
❌ 你不能在 Base Pass 里计算所有 Important 光源的影响(Unity 不会传它们的数据给你)。
可以在frag里面遍历吗
功能层面:完全可行!(完全不可行,草,说假的)
VERTEXLIGHT_ON
宏作用范围有限
-
这个宏 只在顶点着色器阶段有效,在 fragment 中是 始终未定义 的 Unity Answers+8Unity Answers+8Unity Answers+8。
-
如果你用
Shade4PointLights(...)
,它 必须写在 vertex shader(vert
)里,然后把结果传递给 fragment。
把光的作用放到vert函数里面就好了
Shader "Custom/VertexMultiPointSinglePass" {Properties {_Diffuse ("Diffuse", Color) = (1,1,1,1)_Specular ("Specular", Color) = (1,1,1,1)_Gloss ("Gloss", Range(8,256)) = 20}SubShader {Tags { "RenderType"="Opaque" }LOD 200Pass {Tags { "LightMode"="ForwardBase" }CGPROGRAM#pragma target 3.0// 启用 ForwardBase 变体 + 顶点光支持#pragma multi_compile_fwdbase _ VERTEXLIGHT_ON#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"#include "Lighting.cginc"fixed4 _Diffuse;fixed4 _Specular;float _Gloss;struct a2v {float4 vertex : POSITION;float3 normal : NORMAL;};struct v2f {float4 pos : SV_POSITION;float3 worldNormal : TEXCOORD0;float3 worldPos : TEXCOORD1;fixed3 vertLight : TEXCOORD2;};v2f vert(a2v v) {v2f o;o.pos = UnityObjectToClipPos(v.vertex);o.worldNormal = UnityObjectToWorldNormal(v.normal);o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;#ifdef VERTEXLIGHT_ON// 累加最多 4 盏顶点光o.vertLight = Shade4PointLights(unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,unity_LightColor[0].rgb, unity_LightColor[1].rgb,unity_LightColor[2].rgb, unity_LightColor[3].rgb,unity_4LightAtten0,o.worldPos, o.worldNormal);#elseo.vertLight = 0;#endifreturn o;}fixed4 frag(v2f i) : SV_Target {fixed3 N = normalize(i.worldNormal);// 基础环境 + 顶点光fixed3 col = UNITY_LIGHTMODEL_AMBIENT.xyz + i.vertLight;// 主方向光(逐像素处理)fixed3 L = normalize(_WorldSpaceLightPos0.xyz);fixed3 diff = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(N, L));fixed3 V = normalize(_WorldSpaceCameraPos.xyz - i.worldPos);fixed3 H = normalize(L + V);fixed3 spec = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(N, H)), _Gloss);col += diff + spec;return fixed4(col, 1);}ENDCG}}FallBack "Specular"
}
Fragment shader 能读到 Unity 传的光源数组(如 unity_LightPosition[i]
、unity_LightColor[i]
),你可以编写如下逻辑:
fixed3 accum = UNITY_LIGHTMODEL_AMBIENT.rgb;
for (int i = 0; i < 4; ++i) {
float3 L = unity_LightPosition[i].xyz - worldPos;
float nDotL = max(0, dot(normalize(i.worldNormal), normalize(L)));
accum += unity_LightColor[i].rgb * nDotL;
}
return fixed4(accum, 1);
只要光源被识别为顶点光或 SH,它们的数据就存在,任何位置都能遍历。
性能与编译影响要注意
-
循环和分支可能不被 GPU 友好
GPU 通常会对光照循环进行“展开”(unroll),Unity 限定了最大 4 个顶点光,避免过度展开带性能问题。Reddit -
顶点 vs 片元处理成本差异-
在 顶点着色器 里做计算只会执行一次/顶点,会将结果插值至片元,性能更高。 -
在 片元着色器 里做执行次数是按照像素级别做循环,成本显著更高。kylehalladay.com+6Unity Documentation+6Unity Discussions+6Game Development Stack Exchange
-
建议做法
frag 做循环,完成功能但性能次之 | |
vert ,只跑一次 ➜ 储存到 TEXCOORD ,最终插值到 frag |
没有出现四个点光源的作用
思路是正确的,但目前代码中存在几个关键问题导致顶点光(unity_LightPosition / unity_LightColor)无法正常发挥作用:
这个也不用管,可以直接去掉,但是保险起见加上也没有关系
Unity 不会自动启用顶点光宏 VERTEXLIGHT_ON
VERTEXLIGHT_ON
你在 Base Pass 中使用了:
#pragma multi_compile_fwdbase
但是,这不会自动生成支持顶点光的变体。Unity 必须明确知道你支持“顶点光模式”,即你需要:
#pragma multi_compile_fwdbase _ VERTEXLIGHT_ON
这样 Unity 才会传递 unity_LightPosition[i]
、unity_LightColor[i]
等数据,并使用 VERTEXLIGHT_ON
进行编译分支切换。否则,数组永远为空。
float3 Shade4PointLights(
unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
unity_LightColor[0].rgb, ...,
unity_4LightAtten0,
worldPos, worldNormal)
必须在 vert 中使用,把计算结果打包到传给 fragment 的 v2f(如 LIGHTING_COORDS 或 TEXCOORD)。
xxxx
这样用点光,就显示是一个pass了,没有additional pass
https://github.com/candycat1992/Unity_Shaders_Book?tab=readme-ov-file
属性里显示的是当前坐标系下的坐标?显示是世界坐标?gpt说是始终世界坐标,但是添加了约束的笔一直没有变化,但是却一直跟着笔在移动
【Blender】骨骼子级约束在动画中的相关应用(连接与断开)_哔哩哔哩_bilibili
baked成动画,才可以在unity里跟着动
Blender子级约束(Child of)真的弄明白了吗?——1_哔哩哔哩_bilibili
“Child Of” 约束,这是一个非常强大的父子关系模拟器(模拟而已,实际上不是)
它允许你让一个物体“临时成为另一个物体的子物体”,但同时可以灵活控制哪些变换(位置/旋转/缩放)被继承。
Vertex Group
-
(可选)仅当你的对象是**网格(Mesh)**时生效。
-
如果指定了一个顶点组,约束只对那个区域部分生效(局部影响)
-
对于笔或球这种刚体物体来说,一般不填。
Transform Axes: Location
, Rotation
, Scale
这些决定你的物体会继承目标物体哪些变换:
选项 | 作用 |
---|---|
Location X/Y/Z | 继承 Cube 的平移移动 |
Rotation X/Y/Z | 继承 Cube 的旋转(旋转会影响方向) |
Scale X/Y/Z | 继承 Cube 的缩放(如果 Cube 放大,子物体也会变大) |
如果你只想让物体“跟着移动”,可以只勾选 Location。
如果只想方向对齐,不移动位置,可以只勾 Rotation。
Influence(影响力)
-
范围:
0.0 ~ 1.0
,默认是 1。 -
控制这个约束的作用强度:
-
1.0
:完全跟随父物体; -
0.0
:完全忽略该约束; -
中间值:渐进式过渡(适合动画混合、约束切换)
-
会看见两个骨架的顶点组
传统 Shadow Mapping和SSShadow有什么区别?
传统 Shadow Mapping(基于光源视角) 与 屏幕空间阴影(SSShadow、SSAO风格) 之间在 渲染阶段、坐标空间、数据来源、实时性与性能代价 等多方面的核心区别。
传统阴影映射是从光源视角生成深度图,再投影判断阴影;
而屏幕空间阴影是直接在相机最终图像上,用深度或法线等 GBuffer 信息“后处理”生成阴影效果。
维度 | 传统 Shadow Mapping | 屏幕空间阴影 (Screen Space Shadows) |
---|---|---|
原理 | 光源视角下生成 Depth Map,再在主相机中采样投影判断 | 基于相机渲染结果的深度图/法线图,后处理生成阴影 |
坐标空间 | 世界空间 ➜ 光源空间 ➜ ShadowMap 贴图空间 | 完全是屏幕空间(camera-space) |
需要额外 Pass 吗 | ✅ 是,光源需要渲染 ShadowCaster Pass | ❌ 否,在 camera pass 后处理阶段进行 |
精度问题 | 有:Shadow Acne / Peter Panning / Bias 问题 | 无传统投影偏差,但有深度模糊误差 |
可以支持哪些光源? | 点光、聚光、方向光等皆可 | 通常只支持方向光,模拟局部阴影较弱 |
是否支持物体遮挡(Occlusion) | ✅ 准确遮挡 | ❌ 通常依赖近似,不支持遮挡后物体 |
对透明物体支持 | ❌ 差,需要特殊处理 | ❌ 屏幕空间也难以支持透明体阴影 |
性能消耗 | 成本在于额外光源Pass + 深度贴图采样 | 成本在于屏幕像素遍历 + GBuffer采样 |
阴影柔化 | PCF / VSM / ESM 等硬件技术 | 屏幕空间模糊或基于深度差值卷积 |
典型用途 | 通用阴影系统,适用于所有真实光照 | 主用于 Directional Light 的模糊阴影增强 |
代表技术 | Unity/Unreal 内置 Shadow Mapping | Unity URP/HDRP 中的 "Screen Space Shadows"、SSAO、Raytraced Shadows |
🎬 图像感受上举例:
类型 | 显示效果 |
---|---|
传统 Shadow Map | 一束光照打下来,投影后落到场景中,边缘清晰或软化(PCF等) |
屏幕空间阴影 | 相机看到的物体边缘“附着”一层柔化的影子,随视角而改变,阴影只能出现在当前屏幕可见物体上 |
-
传统 Shadow Mapping 是真实物理空间模拟 ➜ 优点是遮挡真实、兼容性高;缺点是容易出错,需要多个 Pass;
-
屏幕空间阴影是视觉欺骗的后处理 ➜ 优点是快速灵活、节省 Pass;缺点是不可穿透、不支持远处和透明对象、贴在图像上。
Screen space shadows
https://shahriyarshahrabi.medium.com/custom-shadow-mapping-in-unity-c42a81e1bbf8
Unity 中的 “Light Cookie”
在 Unity 中,“Cookie” 不是浏览器 cookie,而是 一种纹理遮罩(mask),用于控制灯光形状效果:
-
本质:一张灰度或透明纹理(只取 Alpha 通道)
-
作用:改变光源投射的形状或强度
-
效果:模拟树荫、窗户光斑、Caustics 光斑等复杂光形状YouTube+11Unity Manual+11Medium+11Medium+3Medium+3YouTube+3
你可以在 Light 组件中设置 Cookie 属性,将纹理拖入即可,Unity 会把光线限制在纹理遮罩区域之内,形状随纹理透明度变化。对于点光与聚光灯可使用球面映射贴图(Cubemap),方向光也可平铺或单次投射Medium。
开启 Cookie 对性能影响很小,但能极大提升灯光氛围感。
Unity 官方明确状态(从文档来看):
使用 VertexLit shader 的物体 无法接收阴影,但仍能 投射阴影。Unity Documentation+11Unity Documentation+11Unity Discussions+11
即便是在 Unity 2022 的版本中(虽然文档基于早期版本,但 VertexLit 是 Unity 最原始的像素光照模型,Unity 并未改变其本质行为),其不支持从主视角接收阴影仍然成立。
社区反馈情况:
-
多年社区讨论中均指出:即便自定义 Shader 使用
FallBack "VertexLit"
,也不会在主摄像机中显示阴影,只有投射阴影可见。Unity DiscussionsUnity Discussions -
最新 URP 或 Built‑in RP 实践中,同样通过社区测试反复得出:VertexLit 无法接收阴影。
目前没有官方说明或社区反馈表明 Unity 更改了 VertexLit shader 的行为,它仍是最简化版本:
-
不支持 SHADOW_RECEIVER(不参与阴影比较)
-
即便你自定义写 ShadowCaster pass,fallback 本身仍不会接收阴影。
cull是显示正反面,而Cast shadow的two side和cull是分开的
ablend 这样一般不投射阴影,或者force to apply shadow
而a cut ,这样的做法是自己给一个property,然后以此为阈值去切除a低的
(但这种使用方法应该是可以扩展的,得到某些特殊的投影效果,比如让投影有那种线条的分割感,用某个遮罩通道单独做一个,得出特殊的投影效果)
即使你没显式写 TRANSFER_SHADOW
、UNITY_LIGHT_ATTENUATION
,而 Unity 2020 版本的编译器可能自动插入 shadow receiver variant,让你的 VertexLit shader 实际具备阴影接收能力。
但这种机制并非文档推荐,也可能在 Unity 2022+ 或 URP 中失效,所以可视为“隐藏行为”。
📌 官方行为说明
Unity Manual 表示:
“
Fallback "Name"
会将目标 Shader 的所有 SubShader 插入到当前 Shader 后面” Reddit+14Huihoo+14Unity Documentation+14。
换言之,使用 Fallback "VertexLit"
,Unity 会自动加入系统的 Legacy VertexLit
Shader 的所有 SubShader 和 Pass,相当于额外创建了它的 ShadowCaster 和可能的 Receiver Pass。
🧪 社区实证补充
社区开发者指出:
“
FallBack Off
会导致 Surface Shader 无法接收阴影,因为它不包含 VertexLit 的 shadow 接收逻辑” Unity Discussions+5Unity Discussions+5Unity Discussions+5。
另一份资源也明确说明:
“VertexLit shader 包含用来进行 Shadow Caster 和 Receiver 的 Pass” Unity Discussions+15Unity Discussions+15Unity Discussions+15。
Unity 默认将物体按照 Render Queue 分组渲染,顺序如下:
-
Geometry(不透明):Default = 2000
-
AlphaTest(Alpha 贴图测试):约 2450
-
Transparent(半透明/Alpha Blend):约 3000
-
Overlay(特效叠加层):约 4000
Unity Discussions+10Unity Documentation+10Unity Discussions+10Game Development Stack Exchange+13Unity Documentation+13Unity Documentation+13
也就是说,所有 Transparent 排序的物体会在不透明之后渲染,你代码中设置 Queue="Transparent"
已经指定为透明队列。
阶段 | 描述 |
---|---|
不透明后绘制透明 | ✅ 默认顺序 |
半透明写入深度? | ❌ 不写(ZWrite Off) |
半透明遮挡半透明? | ✅ 若深度测试失败被丢弃颜色,不会写深度 |
半透明不会因顺序问题全体舍弃 | ✅ 渲染依然会继续 |
ZTest(Depth Test,深度测试)
-
控制当前片元是否应该被绘制。
-
根据当前片元的深度与已有的深度缓冲值比较:
-
默认是
Less
,意味着只有当当前片元的深度小于(更靠近摄像机)已有深度时,才通过测试并绘制。
-
-
常见值包括:
Less
,LEqual
,Greater
,Always
,Disabled
等。 -
具体作用如
ZTest Always
绕过深度测试、总是绘制;ZTest Less
则为常规遮挡测试。
🔹 ZWrite(Depth Write,深度写入)
-
控制当前通过测试的片元是否写入深度缓冲。
-
开启(On) 会将当前片元深度写入深度缓存,影响后续绘制;
-
关闭(Off) 则不会写入,也不会阻挡后续片元。
-
通常在屏幕空间排序和透明渲染时关闭 (
ZWrite Off
),避免透明物体遮挡自身或其他内容
使用场景对比
场景描述 | 建议设置 | 效果 |
---|---|---|
不透明物体 | ZTest Less + ZWrite On | 正常遮挡,同步写入深度缓冲 |
半透明物体 (Blend) | ZTest Less + ZWrite Off | 如果后台不透明遮挡则跳过,且不写深度 |
始终可见特效 | ZTest Always + ZWrite Off | 总是画在最前方,不影响深度缓冲 |
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
表示:
uv.xy 是主纹理 _MainTex 的 UV 坐标,进行 tiling + offset;
uv.zw 是法线贴图 _BumpMap 的 UV 坐标,单独做了一套变换;
此时 o.uv 中就包含两套 UV 信息,因此 float4 是合理且必要的。
Unity 渲染队列(Render Queue)详解
按照 Unity 官方文档,内置渲染管线中存在五大默认队列组合:
队列名 | 数值 | 用途说明 |
---|---|---|
Background | 1000 | 背景预设物体(Skybox背后背景) |
Geometry | 2000 | 默认不透明物体(Opaque) |
AlphaTest | 2450 | 只读 Alpha 测试(如裁剪地形) |
Transparent | 3000 | 半透明渲染(Alpha Blend) |
Overlay | 4000 | UI和特效类渲染(UI层) |
ComputeGrabScreenPos 的功能
-
它是 UnityCG.cginc 提供的屏幕空间坐标辅助函数,输入是顶点的剪裁空间位置(clip space);
-
返回一个
float4
,其中xyzw
可以直接用于tex2Dproj(_GrabTexture, ... )
或_GrabScreenColor()
等 Sampling 操作; -
自动处理平台差异、坐标系反转等问题,确保 GrabPass 纹理采样正确采到物体背后的画面。
ComputeGrabScreenPos(o.pos)
会将当前顶点的 clip-space 坐标转换为可用于访问 GrabPass 屏幕纹理的 UV 坐标(scrPos),以便后续 fragment shader 正确采样背景画面。
xxx
为什么需要先转换成这种坐标系才可以使用贴图?我的模型已经了uv,现在又有了图,直接用不行吗
原因是 —— 采样的贴图类型不同,本质坐标系也不同。
贴图用途 | 对应坐标 | 坐标来源 |
---|---|---|
模型贴图(如 BaseMap、NormalMap) | 模型 UV(v.texcoord.xy ) | 模型网格自带的 UV 坐标 |
屏幕贴图(如 GrabPass、屏幕遮罩) | 屏幕空间 UV(ComputeGrabScreenPos() ) | Clip Space ➜ NDC ➜ Screen UV |
后处理贴图(如 SSAO、Bloom) | 屏幕空间 UV | Camera 渲染出来的屏幕图像 |
模型贴图(使用模型 UV)
fixed4 tex = tex2D(_MainTex, i.uv);
-
i.uv
是模型提供的TEXCOORD0
; -
对应
BaseColor
,Normal
,Roughness
等; -
不受相机或屏幕影响,只与模型本身有关。
屏幕贴图(如玻璃、水面后面的画面)
o.scrPos = ComputeGrabScreenPos(o.pos);
...
half4 screenColor = tex2Dproj(_GrabTexture, i.scrPos);
-
这里
_GrabTexture
是当前屏幕内容; -
我们并不是采样模型上的纹理,而是采样“模型后面原本应该看到的画面”;
-
所以,必须转为屏幕空间坐标(
clip space ➜ NDC ➜ screen UV
)去采样。
grabpass是整个屏幕的一张图,还是说这个材质上 的物体的覆盖范围的一张图
answer一针见血:
GrabPass
抓的是整个屏幕的一张图(即当前所有已渲染内容,不是当前物体本身的部分)。
所以需要;
所以需要这个模型在屏幕空间下的各个像素的位置ComputeGrabScreenPos(o.pos),
才可以进一步的只在这个模型范围之内做手脚,修改各种看到的效果
为什么要乘上 i.scrPos.z
?
-
i.scrPos.z
是 从相机到该像素的深度(NDC 空间) -
越远的像素 → 折射偏移越大 → 更真实的折射透视感
根据深度加权地扰动屏幕坐标,实现 视差折射感(parallax refraction)。
除以 w
正是将 齐次裁剪空间坐标 (clip space) 转换为 屏幕空间 UV 坐标 的标准步骤
o.pos = UnityObjectToClipPos(v.vertex);
这是顶点经过变换后处于裁剪空间 (Clip Space) 的坐标:
o.pos = float4(x, y, z, w)
它表示该顶点相对于当前摄像机的位置,还未被除以 w,不是真正的屏幕位置。
【教程】技术美术入门:渲染管线概述_哔哩哔哩_bilibili
w 分量的来源
-
最初,模型顶点在 Object Space(模型局部空间)以齐次坐标形式表示,通常是
(x, y, z, 1)
。
假设一个模型顶点在 Object Space 下的坐标:
v_model = (x=1, y=2, z=3, w=1)
其中 w=1
是齐次坐标的默认值,表示这是一个位置向量。
🧭 1. 模型 → 世界 → 视图空间(Camera/View Space)
先通过模型矩阵(Model)和视图矩阵(View)变换:
v_view = ViewMatrix * ModelMatrix * v_model
-
假设变换后得到(示例):
v_view = (vx=5, vy=4, vz=−10, vw=1)
vw
通常保持为 1,因为只是线性/平移变换。
🧮 2. 视图空间 → Clip Space(乘以投影矩阵)
接着,把上面得到的 v_view
乘以 Projection 矩阵 P
(Perspective Projection):
v_clip = P * v_view = (x_c, y_c, z_c, w_c)
一个典型的透视投影矩阵会把 -z
放到 w
分量中,因此:
w_c = -v_view.z = 10
(这是一种简化演示,具体值由 near/far plane 设定决定)
最终可能得到:
v_clip = (x_c=2.0, y_c=1.6, z_c= (some value), w_c=10)
clip空间里的xy可以说就是对齐屏幕了,z只是被压缩了,但是里面的前后关系依然清晰可见
(如何压缩的,取决于view space里的时候,相机的near和far两个面是怎么样的数值)
clip里,此时的w则是保存了原本的-z(更加直观的对比出前后的关系)
然后通过都除以w进入ndc
而这个-z在之后转换成屏幕坐标后(就变成三维了,)
Clip Space 的 XY 坐标在剔除后接近屏幕对齐,而 Z 被压缩,但仍保留“远近关系”。
这种压缩方式由 near/far 平面 决定;
Clip Space 的
w
分量则基本是来自 camera/view space 的-z
,也就是摄像机空间深度;最后经过 除以 w(也就是透视除法),进入 NDC,再映射到屏幕空间。
----总之,不用管,里面都是乱七八糟的设计,好在哪也和你没关系,这个w爱怎么样怎么样
clip space记住是压缩成了长方体了,
裁剪空间的定义要求所有坐标都在 [-w, w] 范围内。
P矩阵的设计目标正是让 view space 中的几何符合这个标准,而将 z 的远近信息藏进 w 中,以供透视除法恢复远近关系。
ScreenPos = ViewportTransform(NDC)
这时的 ScreenPos = (x, y, z) 是:
x, y:屏幕上的像素坐标(以像素为单位)或 UV 坐标(单位化的 [0,1] 区域)
z:深度值(通常 ∈ [0, 1]),来自 NDC 的 z 值映射
阶段 | xy 所在范围 | 是否已是 [0,1] UV | 是否需要除以 w |
---|---|---|---|
ComputeScreenPos 输出的 .xy | [-w, +w] → [0, w] | ❌ 不是 | ✅ 必须 |
除以 .w 之后 | Clamped to [0,1] | ✅ 是 | — |
NDC(真实 UV) | [0,1] | ✅ 是 | 已完成 |
NDC (Normalized Device Coordinates)
-
这是在 裁剪空间进行透视除法之后得到的坐标空间。
-
坐标值通常属于:
-
x, y:范围是
[-1, +1]
(OpenGL),或已转换成[0,1]
的 UV 空间(Unity 默认映射到 UV)。 -
z:范围是
[0,1]
或[-1,1]
,具体取决于 API/D3D/OpenGL 等。
-
-
只有当顶点经过 clipPos.xyz / clipPos.w 后,才进入 NDC(标准化设备坐标)。
ComputeScreenPos(clipPos) 的功能(Unity 内部)
-
它接受的是裁剪空间坐标
float4 clipPos
,其.xy
分量的是还处在 “齐次空间”—— 即仍然属于[-w, +w]
区间。 -
ComputeScreenPos 会对
xy
做((-w, +w) → (0, +w))
的线性映射:"transform input from clip coordinate vertex position [-w, w] into [0, w]" Unity Discussions+1Unity User Manual+1GitHub+11Unity Discussions+11cyanilux.com+11Game Development Stack Exchange+5cyanilux.com+5Stack Overflow+5Unity Documentation+2Unity Documentation+2Unity User Manual+2
-
然后通过 透视除法(片段里用
xy / w
)才能真正归一化到[0,1]
范围 Unity DiscussionsUnity Discussions。