44.Shader开发入门总结
44.1 核心要点速览
光照模型
光照模型概述
- 问题:已知管线、向量矩阵与 ShaderLab/CG 语法后,屏幕上「亮多少、什么颜色」仍要单独建模。
- 定义:光照模型是用公式近似描述光与材质相互作用的算法,决定表面亮度与颜色(入射光、材质参数与交互规律共同作用)。
- 学习主线:漫反射(墙壁、纸张等粗糙面)→ 高光(金属、塑料等镜面感)→ 综合模型(环境 + 漫反射 + 高光)→ Unity 内置方向/空间辅助函数减少手写矩阵。
- 要点:它是工程上可实现的近似,不是唯一真理;Shader 里选对模型、算对空间,比死记名词更重要。
兰伯特漫反射
- 问题:粗糙面各向反射较均匀,如何用简单式子表达「朝灯亮、背灯暗」?
- 分析:漫反射强度由 法线与光源方向夹角 决定,夹角越小,余弦越大,越亮。背光侧点积为负时需截断,避免「负亮度」。
- 公式:( \mathbf{n},\mathbf{l} ) 取单位向量,点积即 ( \cos\theta )。
\[
C_d = \mathbf{L} \odot \mathbf{M}_d \cdot \max(0, \mathbf{n}\cdot\mathbf{l})
\]
- 实践要点:
Lighting.cginc中_LightColor0;平行光方向用_WorldSpaceLightPos0.xyz并归一化;法线用UnityObjectToWorldNormal转到世界空间;常加UNITY_LIGHTMODEL_AMBIENT.rgb打底,避免阴影区死黑。
半兰伯特漫反射
- 问题:纯兰伯特背光面可整块为 0,卡通、插画需要背光仍有灰阶。
- 分析:在 观感 上做文章,把 ( \mathbf{n}\cdot\mathbf{l}\in[-1,1] ) 线性映射到 ( [0,1] ),不声称比兰伯特更物理。
- 公式:
\[
C_d^{\mathrm{half}} = \mathbf{L} \odot \mathbf{M}_d \cdot \left(\frac{\mathbf{n}\cdot\mathbf{l}}{2}+\frac{1}{2}\right)
\]
Phong 式高光反射
- 问题:镜面感来自「视线是否对准反射光方向」,如何量化?
- 分析:先算反射方向 ( \mathbf{r}=\mathrm{reflect}(-\mathbf{l},\mathbf{n}) ),再看 ( \mathbf{v} ) 与 ( \mathbf{r} ) 是否接近;用
pow抬指数 ( n ) 控制高光锐利度。 - 公式:
\[
C_s = \mathbf{L} \odot \mathbf{M}_s \cdot \max(0,\mathbf{v}\cdot\mathbf{r})^{n},\quad \mathbf{r}=\mathrm{reflect}(-\mathbf{l},\mathbf{n})
\]
- 实践要点:
reflect的入射方向与「光从哪来」符号约定要一致(常用-光源方向);视角方向由_WorldSpaceCameraPos与顶点世界坐标构造并归一化。
Blinn-Phong 式高光反射
- 问题:每像素算
reflect略繁,且 Phong 高光在部分角度偏「硬」。 - 分析:用 半角向量 ( \mathbf{h}=\mathrm{normalize}(\mathbf{v}+\mathbf{l}) ) 代替反射方向,看 ( \mathbf{n} ) 与 ( \mathbf{h} ) 是否接近共线;通常 更快、高光略柔和。
- 公式:
\[
C_s = \mathbf{L} \odot \mathbf{M}_s \cdot \max(0,\mathbf{n}\cdot\mathbf{h})^{n},\quad \mathbf{h}=\mathrm{normalize}(\mathbf{v}+\mathbf{l})
\]
Phong 光照模型(完整)
- 综合:环境光 + 兰伯特漫反射 + Phong 高光,三者 颜色相加(向更亮方向叠加,符合直觉上的多光源贡献)。
- 公式:
\[
C = C_{\mathrm{amb}} + C_d^{\mathrm{Lambert}} + C_s^{\mathrm{Phong}}
\]
- 环境光:
UNITY_LIGHTMODEL_AMBIENT,或在 Lighting 里用天空盒、渐变、unity_AmbientSky等;漫反射、高光项同前。
Blinn-Phong 光照模型(完整)
- 综合:与 Phong 相同的三项结构,仅把高光换成 Blinn-Phong 项;实时渲染里很常见。
- 公式:
\[
C = C_{\mathrm{amb}} + C_d^{\mathrm{Lambert}} + C_s
\]
其中 ( C_s ) 按 Blinn-Phong 高光 式计算(半角向量版)。
光照模型对照表
| 名称 | 核心思路 | 公式骨架 | 关键参数与特点 |
|---|---|---|---|
| 兰伯特漫反射 | 法线与光方向夹角越小越亮 | ( \mathbf{L}\odot\mathbf{M}_d\cdot\max(0,\mathbf{n}\cdot\mathbf{l}) ) | 世界空间法线、光向;背光可全黑 |
| 半兰伯特 | 点积映射到 ( [0,1] ),背光有层次 | 上式中把 ( \max(0,\cdot) ) 换为 ( \frac{\mathbf{n}\cdot\mathbf{l}}{2}+\frac{1}{2} ) | 风格化友好,非能量守恒物理模型 |
| Phong 高光 | 视线与反射方向对齐程度 | ( \propto \max(0,\mathbf{v}\cdot\mathbf{r})^{n} ) | reflect(-光向, 法线);( n ) 越大越尖 |
| Blinn-Phong 高光 | 法线与半角对齐程度 | ( \propto \max(0,\mathbf{n}\cdot\mathbf{h})^{n} ) | 少 reflect,常更快、略柔和 |
| Phong 光照模型 | 环境 + 兰伯特 + Phong 高光 | 三项相加 | 经典完整模型 |
| Blinn-Phong 光照模型 | 环境 + 兰伯特 + Blinn-Phong 高光 | 三项相加 | 游戏与引擎中更常用 |
- 名词分清:「Phong/Blinn-Phong 高光」只负责镜面项;「Phong/Blinn-Phong 光照模型」指 整碗汤(环境 + 漫反射 + 高光)。
- 效率:同条件下 Blinn-Phong 高光常省掉显式反射向量,更适合逐像素计算密集的实时场景。
逐顶点与逐片元光照
- 类比:顶点光照像在 几个角 算好明暗再插值涂满三角面,省算力但易糊;片元光照是 每个像素 用自身插值后的法线、位置再算,更准更贵。
- 原因:顶点间颜色由光栅化 插值 得到,无法表现细尺度明暗;片元阶段可再用高精度法线(如法线贴图)参与计算。
- 性能:顶点着色器按顶点计费,片元着色器按覆盖像素计费;同模型下移计算到片元通常更耗。
Unity 内置常用函数(UnityCG.cginc)
- 作用:统一处理「从模型空间顶点出发」的观察方向、光照方向与矩阵变换,减少手写出错。
- 观察方向:
WorldSpaceViewDir(v)(输入模型空间顶点)、UnityWorldSpaceViewDir(v)(输入世界空间顶点)、ObjSpaceViewDir(v)(模型空间方向)。 - 光照方向(Forward 渲染):
WorldSpaceLightDir(v)、UnityWorldSpaceLightDir(v)、ObjSpaceLightDir(v);返回值 **常需normalize**,且与管线版本、宏有关,以工程实际 include 为准。 - 空间与裁剪:
UnityObjectToWorldNormal、UnityObjectToWorldDir、UnityWorldToObjectDir、UnityObjectToClipPos。
纹理
纹理与 UV 在干什么
- 问题:模型只有几何,如何贴上「皮」?
- 分析:美术在 DCC 里为顶点写入 UV;GPU 光栅化时对 UV 插值,片元里用
tex2D取色。UV 一般归一化到 ( [0,1] ),与纹理分辨率无关。 - 实践:
Properties声明2D,CG 中sampler2D _MainTex与float4 _MainTex_ST(xy 缩放、zw 偏移);顶点里uv = texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw或TRANSFORM_TEX。
导入设置:Wrap 与 Filter
- Wrap Mode:
Repeat像瓷砖重复;Clamp钳住边缘防绕回;还有镜像等变体,按美术需求选。 - Filter Mode:
Point硬边像素风;Bilinear/Trilinear缩放更柔,常配合 Mipmap 减闪烁与走样。
单张纹理 + Blinn-Phong
- 问题:贴图颜色与光照如何合成才不假灰、不假白?
- 分析:把采样结果当作 反照率 albedo:
albedo = texColor × 漫反射色;兰伯特项用albedo;环境光也乘albedo,避免「环境一块灰蒙在模型上」。 - 综合:
最终 ≈ 环境光×albedo + 漫反射项 + Blinn-Phong 高光项(与系列课文一致即可)。
凹凸纹理(法线贴图)
- 问题:不加三角面,如何做出细小凹凸?
- 分析:用贴图存 扰动后的法线,光照仍按新法线算,眼睛感到起伏。高度图 灰度存高度,常需额外求导变正法线,开销大;法线贴图 直接存法线向量更常用。
- 空间选择:切线空间 法线偏蓝紫,易复用到不同朝向表面;模型空间 法线彩色、直观但换模型常要重烘焙。世界空间 算法线光照利于点光等,但矩阵与变体更多。
- 实践套路:
appdata_full带切线;副切线bitangent = cross(n, t) * tangent.w;构建TBN;UnpackNormal解码;_BumpScale控制强度。切线空间算法:光向、视向转到切线空间再dot;世界空间算法:法线转到世界空间再与光向、半角算。
渐变纹理(Ramp)
- 问题:如何把连续明暗变成 色带、插画感?
- 分析:用半兰伯特标量 ( t=\frac{\mathbf{n}\cdot\mathbf{l}}{2}+\frac{1}{2}\in[0,1] ) 当作 UV(如
float2(t,t))采样_RampTex,用纹理颜色 重映射 明暗,而非直接用标量当亮度。 - 实践:渐变图
Wrap Mode建议Clamp,避免浮点误差在Repeat下绕一圈采到缝上的 黑点。与凹凸结合时:在切线空间 Shader 上把漫反射项替换为「半兰伯特 → Ramp → 乘光色与 albedo」,高光仍可保留 Blinn-Phong。
遮罩纹理
- 问题:同一材质上,有的区域要更强高光、有的区域要压住特效?
- 分析:用贴图某一通道(如 R)作 局部权重,再乘自定义系数
_SpecularScale,最后乘进高光项。RGBA 四路可分别存高光、透明、特效等,减少贴图张数。
透明
前置概念:队列、深度、测试、混合
- **渲染队列
Queue**:决定大致绘制顺序;透明物体通常排在不透明之后,才能读到背后颜色做混合。 - 深度缓冲:每像素记「目前最近深度」;深度测试决定新片元是否通过;混合决定通过后的颜色如何与颜色缓冲已有值合成。
- 直觉:不透明物体常开深度写入,顺序不敏感;半透明常 关
ZWrite才能混合背后,但引入 排序敏感(前后穿插时可能错)。
深度写入、队列与混合(Shader 里怎么写)
- **
ZWrite**:写在 Pass 内,只影响该 Pass。半透明 Pass 常见ZWrite Off。 - **
Queue与RenderType**:写在 SubShader 的Tags,影响其下所有 Pass。
Pass
{
Tags { "LightMode"="ForwardBase" }
ZWrite Off
}
SubShader
{
Tags{ "Queue" = "AlphaTest" "IgnoreProjector" = "True" "RenderType" = "TransparentCutout" }
}
SubShader
{
Tags{ "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent" }
}
- 混合通式(RGB 与 A 可分别指定因子;具体以目标平台文档为准):
\[
O_{\mathrm{rgb}} = F_s \cdot S_{\mathrm{rgb}} + F_d \cdot D_{\mathrm{rgb}}
\]
常见 透明度混合:Blend SrcAlpha OneMinusSrcAlpha,即
\[
\mathbf{C}_{\mathrm{out}} = \alpha \mathbf{C}_S + (1-\alpha)\mathbf{C}_D
\]
透明度测试(Alpha Test)
- 场景:树叶、栅栏、草等 镂空,不要真半透明。
- 做法:
clip(texColor.a - _Cutoff),低于阈值直接丢弃片元;通过的可按不透明写深度。Queue=AlphaTest,RenderType=TransparentCutout;不必关深度写入。
透明度混合
- 场景:玻璃、水、特效半透明层。
- 做法:
Queue=Transparent,ZWrite Off,Blend SrcAlpha OneMinusSrcAlpha;返回颜色的 alpha 常用texColor.a * _AlphaScale。注意整体渲染顺序与穿插。
双 Pass:先钉深度再混合
- 问题:复杂网格自穿插时,单纯关
ZWrite易导致片元顺序错乱。 - 解决:第一 Pass
ZWrite On且ColorMask 0只写深度;第二 Pass 正常半透明混合。代价是多一遍几何;模型内部一般不互相混合,仅最前层参与混合。
双面透明
- **
Cull**:Cull Back默认只正面;Cull Front只背面;Cull Off双面。 - Alpha Test:常
Cull Off即可双面镂空。 - Alpha Blend:常用两 Pass——先
Cull Front画背,再Cull Back画前,保证混合顺序与薄壳玻璃观感。
44.2 面试题精选
基础题
1. 兰伯特与半兰伯特漫反射的区别
题目
兰伯特漫反射与半兰伯特漫反射各如何计算?半兰伯特解决了什么视觉问题?它是否更「物理正确」?
深入解析
- 兰伯特:(C_d \propto \max(0, \mathbf{n}\cdot\mathbf{l})),背光侧点积为负时被截断为 0,整块背光可显得死黑。
- 半兰伯特:把 (\mathbf{n}\cdot\mathbf{l}) 从 ([-1,1]) 线性映射到 ([0,1]),背光仍有明暗渐变,利于卡通、手绘感。
- 物理性:半兰伯特是视觉近似,不是能量守恒的物理模型;面试里要能说清「为什么好看」和「为什么不等于真实漫反射」。
答题示例
兰伯特用 (\max(0, \mathbf{n}\cdot\mathbf{l})),背光容易全黑。半兰伯特用 (\frac{\mathbf{n}\cdot\mathbf{l}}{2}+\frac{1}{2}) 把点积拉到 0~1,背光也有层次。它是风格化技巧,不强调物理正确。
参考文章
- 3.光照模型-漫反射光照模型-兰伯特光照模型-必备知识点
- 6.光照模型-漫反射光照模型-半兰伯特光照模型-必备知识点
2. Phong 高光与 Blinn-Phong 高光的差异
题目
Phong 高光与 Blinn-Phong 高光在方向量选择上差在哪里?为何实时渲染里更常见 Blinn-Phong?
深入解析
- Phong:用反射方向 (\mathbf{r}=\mathrm{reflect}(-\mathbf{l},\mathbf{n})),高光项看 (\mathbf{v}\cdot\mathbf{r})。
- Blinn-Phong:用半角向量 (\mathbf{h}=\mathrm{normalize}(\mathbf{v}+\mathbf{l})),高光项看 (\mathbf{n}\cdot\mathbf{h})。
- 工程上:避免显式求反射向量、高光形状往往更稳;代价是「在相同指数 (n) 下」与 Phong 高光宽度并不完全一致,调参时要心里有数。
答题示例
Phong 用视角和反射方向的夹角;Blinn-Phong 用法线和半角向量的夹角。Blinn-Phong 少算反射方向、GPU 友好,游戏里更常用,但光泽度要和 Phong 一一对应需要重新调。
参考文章
- 9.光照模型-高光反射光照模型-Phong式高光反射模型-必备知识点
- 12.光照模型-高光反射光照模型-BlinnPhong式高光反射模型-必备知识点
3. 透明度测试与透明度混合怎么选
题目
AlphaTest(clip)与半透明 Alpha Blend 各适合什么效果?对深度写入、排序各有什么影响?
深入解析
- 透明度测试:像素要么保留要么
discard,一般可保持ZWrite On,排序压力小,适合镂空、树叶、栅栏。 - 透明度混合:真半透明,常配
ZWrite Off+Blend SrcAlpha OneMinusSrcAlpha,依赖渲染顺序,玻璃、水体、特效常用。 - 易错点:把需要柔和渐变的半透明硬做成 clip,或把镂空当半透明又开深度写入导致层次错误。
答题示例
Clip 是硬切,一般还能写深度,适合镂空。半透明要混合背后颜色,通常关深度写入,所以同一批透明物体排序很重要;玻璃、雾状用混合,草叶栅栏多用 AlphaTest。
参考文章
- 40.透明-效果实现-透明度测试
- 41.透明-效果实现-透明度混合
进阶题
1. 逐顶点与逐片元光照的取舍
题目
同样一套光照公式,放在顶点着色器和片元着色器里效果差在哪里?为什么片元更贵?
深入解析
- 逐顶点:光照在顶点上算完,颜色在三角形内插值,高光、法线变化快时容易「糊、条带」。
- 逐片元:每像素用插值后的法线、位置再算,细节好,但指令数随分辨率线性放大。
- 选用:移动端或大量简单物体可顶点光照;角色、法线贴图、锐利高光优先片元。
答题示例
顶点光照是算在顶点上再插值,中间像素不重新算光照,所以高光和法线细节容易糊。片元光照每像素算,贵但准。有法线贴图或要高光质量就上片元。
参考文章
- 2.光照模型-逐顶点光照和逐片元光照
- 21.光照模型-为什么逐片元比逐顶点平滑
2. 半透明为何常关闭深度写入
题目
不透明物体可以乱序画,半透明为什么往往要关 ZWrite?关了以后带来什么问题、引擎一般怎么缓解?
深入解析
- 关 ZWrite 的目的:同一像素要累积「背后已经画好的颜色」,若先写上近处深度,远处半透明片元可能被深度测试挡掉,混合无从谈起。
- 副作用:透明物体之间依赖绘制顺序,交叉、自穿插模型容易穿帮。
- 缓解:按队列排序、拆模型、双 Pass 深度预写、或接受限制并分材质排序。
答题示例
半透明要和 framebuffer 里已有颜色混合,如果还写深度,后面的透明片元可能被深度测试直接扔掉。所以常关深度写入,代价是透明物体谁先画谁后画会影响结果,要靠队列和排序,复杂情况用双 Pass 或拆 mesh。
参考文章
- 36.透明-必备知识点-重要知识回顾
- 37.透明-必备知识点-渲染顺序的重要性
3. 切线空间法线贴图为何更常用
题目
法线贴图为什么多做成切线空间?和模型空间法线比,各有什么代价?
深入解析
- 切线空间:纹素存的是相对表面的扰动,同一张贴图可跨不同朝向、可动画蒙皮复用,美术流水线友好。
- 模型空间:每个纹素对应绝对方向,物体变形、复用差,但实现上有时少一套 TBN 变换。
- Shader 侧:切线空间要在顶点构造 TBN,把光方向或法线转到同一空间再算点积;世界空间法则在世界空间算光照。
答题示例
切线空间里法线是相对表面局部坐标,贴图可以重复用在不同朝向的模型上,动画顶点也不会轻易「法线乱飞」。模型空间法线跟模型绑定,复用差。代价是 Shader 里要 TBN 变换,多一点顶点与矩阵工作。
参考文章
- 27.纹理-凹凸纹理-基本概念
- 28.纹理-凹凸纹理-法线贴图的计算方式
- 29.纹理-凹凸纹理-切线空间下计算
深度题
1. 法线贴图:切线空间与世界空间两条算路
题目
在切线空间采样法线贴图与在世界空间用 TBN 把法线转出来再光照,各适合什么光源与性能权衡?
深入解析
- 切线空间算光照:顶点把 (\mathbf{l})、(\mathbf{v}) 变到切线空间,片元里与解包法线直接
dot,纹理采样与光照同空间,定向光下很顺。 - 世界空间:片元把切线法线乘 TBN 到世界空间,与
_WorldSpaceLightPos0等统一算;点光、聚光、多光源时少重复变换逻辑。 - 性能:切线空间常减少片元里矩阵组合次数;世界空间可能增加插值与寄存器(传 TBN 三向量)。选型看光源种类与变体数量。
答题示例
切线空间是把光向和视角转到切线空间,在片元里和法线贴图解包结果直接点积,定向光很省事。世界空间是把法线用 TBN 转到世界再算,点光源多的时候更自然。性能上要看是少算片元矩阵还是少插值数据,项目里会按光源类型选一条路或做 Shader 变体。
参考文章
- 29.纹理-凹凸纹理-切线空间下计算
- 30.纹理-凹凸纹理-世界空间下计算
2. 双 Pass 深度预写半透明
题目
ColorMask 0 + ZWrite On 的第一个 Pass 加上正常透明混合的第二个 Pass,解决了什么问题?仍有哪些现象是这套方案解决不了的?
深入解析
- 机制:Pass1 只写深度不写颜色,把「模型最前表面」钉在深度缓冲里;Pass2 半透明混合时深度测试能挡住被其它物体遮挡的片元,减轻凸包内部错误混合。
- 局限:同物体内部多层半透明仍不会像真实介质那样层层混合(近似成只有最前层参与);多 Pass 增加带宽与 Overdraw。
- 与排序关系:不能替代所有排序问题,只是减少「同物体内部」一类错误。
答题示例
第一遍只写深度不输出颜色,相当于先把物体最前面的深度定下来;第二遍再半透明混合,深度测试能去掉被挡住的片元,减轻比如头发、树叶那种内部乱序。但它不能让模型内部多层透明都物理正确混合,只是近似,还多一次 Pass 成本。
参考文章
- 42.透明-效果实现-开启深度写入的半透明效果
3. 渐变纹理与 Wrap Mode
题目
用半兰伯特标量去采样渐变纹理做卡通明暗时,为什么常把渐变纹理设为 Clamp?Repeat 下会出现什么现象?
深入解析
- 半兰伯特输出:理论在 ([0,1]),浮点误差可能略大于 1 或略小于 0。
- Repeat:UV 取小数部分,略大于 1 会绕回纹理左侧,出现错误条带或黑点。
- Clamp:越界钳在 0 或 1,明暗稳定在条带两端;与渐变纹理「一维查找表」语义一致。
答题示例
半兰伯特算出来应该是 0~1,但浮点会有 1.0001 这种。Repeat 会取小数部分,采样会跳到纹理另一端,出现噪点或黑条。Clamp 把坐标钉在 0~1 边界,卡通 ramp 一般都用它。
参考文章
- 31.纹理-渐变纹理-基本概念
- 32.纹理-渐变纹理-基础实现
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com