尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

UnityShader入门精要学习笔记:第六章(2)在Unity中实现光照模型——从漫反射到半兰伯特与高光反射的完整Shader骨架

发布时间:2026/9/29 21:15:42

资讯中心
01
ARTICLE

UnityShader入门精要学习笔记:第六章(2)在Unity中实现光照模型——从漫反射到半兰伯特与高光反射的完整Shader骨架

UnityShader入门精要学习笔记:第六章(2)在Unity中实现光照模型——从漫反射到半兰伯特与高光反射的完整Shader骨架
1. 从一次“背光面全黑”的翻车说起如果你正在学 Unity Shader大概率已经写过那个经典的逐顶点漫反射。代码跑起来向光面亮、背光面黑看起来挺对。但当你把胶囊体转个角度或者换一个面数低一点的模型问题就来了明暗交界处出现明显的锯齿背光面直接糊成一坨纯黑完全看不出模型的体积感。这不是你代码写错了而是标准兰伯特光照模型本身的局限。saturate(dot(n, l))把点积为负的部分全部截断成 0背光区域自然没有任何过渡。半兰伯特模型就是来解决这个问题的它把[-1, 1]的点积结果线性映射到[0, 1]让背光面也有明暗变化。这篇笔记聚焦 Unity Shader 中光照模型的代码落地主线是逐顶点/逐像素漫反射、半兰伯特模型和 Blinn-Phong 高光反射。我会给出完整的 Shader 骨架、材质面板参数配置和场景验证步骤你可以直接复制到项目里跑起来对比不同光照模型的视觉差异。适合已经了解 Shader 基本结构、想系统梳理光照模型实现的同学。2. 前置准备TaoToken 与开发环境在开始写 Shader 之前先把工具链理顺。我平时调试 Shader 和查文档时会用 TaoToken 来快速验证一些模型相关的逻辑尤其是需要对比不同光照公式的输出时它能帮我省去不少切换工具的时间。TaoToken 的官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 。如果你需要生成 API Key 来接入自己的工具链可以直接访问 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。模型对话的入口在 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。Unity 这边建议用 2021 LTS 或更新版本内置渲染管线即可。新建一个 3D 场景删掉默认的天空盒Window Rendering Lighting Environment Skybox Material 设为 None这样环境光不会干扰你对漫反射的观察。然后创建一个胶囊体新建一个 Shader 和对应的材质把材质赋给胶囊体。3. 逐顶点漫反射完整 Shader 骨架先上逐顶点漫反射的完整代码。这个版本的计算全部在顶点着色器里完成片元着色器只负责输出插值后的颜色。Shader Chapter6/DiffuseVertexLevel { Properties { _Diffuse (Diffuse, Color) (1,1,1,1) } SubShader { Pass { Tags { LightMode ForwardBase } CGPROGRAM #pragma vertex vert #pragma fragment frag #include Lighting.cginc fixed4 _Diffuse; struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 pos : SV_POSITION; fixed3 color : COLOR; }; v2f vert(a2v v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); fixed3 ambient UNITY_LIGHTMODEL_AMBIENT.xyz; fixed3 worldNormal normalize(mul(v.normal, (float3x3)unity_WorldToObject)); fixed3 worldLight normalize(_WorldSpaceLightPos0.xyz); fixed3 diffuse _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLight)); o.color ambient diffuse; return o; } fixed4 frag(v2f i) : SV_Target { return fixed4(i.color, 1.0); } ENDCG } } FallBack Diffuse }几个关键点。LightMode必须设为ForwardBase否则_LightColor0和_WorldSpaceLightPos0拿不到正确值。法线从模型空间转到世界空间用的是mul(v.normal, (float3x3)unity_WorldToObject)这里利用了法线变换的特殊性——不能直接用unity_ObjectToWorld因为法线要的是逆转置矩阵。Unity 提供了UnityObjectToWorldNormal内置函数后面会讲。材质面板上只有一个_Diffuse颜色属性默认白色。把材质赋给胶囊体场景里放一个平行光调整方向你能看到向光面亮、背光面暗。但仔细看明暗交界处锯齿很明显这就是逐顶点光照的典型问题。4. 逐像素漫反射与半兰伯特模型逐像素漫反射的思路很简单把漫反射计算从顶点着色器搬到片元着色器。顶点着色器只负责传递世界空间法线片元着色器里再做归一化和点积。Shader Chapter6/DiffusePixelLevel { Properties { _Diffuse (Diffuse, Color) (1,1,1,1) } SubShader { Pass { Tags { LightMode ForwardBase } CGPROGRAM #pragma vertex vert #pragma fragment frag #include Lighting.cginc fixed4 _Diffuse; struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 pos : SV_POSITION; float3 worldNormal : TEXCOORD0; }; v2f vert(a2v v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.worldNormal mul(v.normal, (float3x3)unity_WorldToObject); return o; } fixed4 frag(v2f i) : SV_Target { fixed3 ambient UNITY_LIGHTMODEL_AMBIENT.xyz; fixed3 worldNormal normalize(i.worldNormal); fixed3 worldLightDir normalize(_WorldSpaceLightPos0.xyz); fixed3 diffuse _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir)); fixed3 color ambient diffuse; return fixed4(color, 1.0); } ENDCG } } FallBack Diffuse }对比逐顶点版本逐像素的明暗过渡平滑很多锯齿基本消失。但背光面依然是纯黑没有细节。这时候半兰伯特模型就派上用场了。半兰伯特的公式是dot(n, l) * 0.5 0.5把点积结果从[-1, 1]映射到[0, 1]。修改片元着色器里的漫反射计算fixed3 diffuse _LightColor0.rgb * _Diffuse.rgb * (dot(worldNormal, worldLightDir) * 0.5 0.5);就这一行改动背光面立刻有了明暗过渡模型看起来更立体。注意半兰伯特没有物理依据纯粹是视觉上的经验模型但在很多风格化渲染里效果不错。5. 高光反射Phong 与 Blinn-Phong 的代码实现高光反射需要多几个参数高光颜色_Specular和光泽度_Gloss。先看逐像素的 Phong 模型实现。Shader Chapter6/SpecularPixelLevel { Properties { _Diffuse (Diffuse, Color) (1,1,1,1) _Specular (Specular, Color) (1,1,1,1) _Gloss (Gloss, Range(8.0, 256)) 20 } SubShader { Pass { Tags { LightMode ForwardBase } CGPROGRAM #pragma vertex vert #pragma fragment frag #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; }; v2f vert(a2v v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.worldNormal mul(v.normal, (float3x3)unity_WorldToObject); o.worldPos mul(unity_ObjectToWorld, v.vertex).xyz; return o; } fixed4 frag(v2f i) : SV_Target { fixed3 ambient UNITY_LIGHTMODEL_AMBIENT.xyz; fixed3 worldNormal normalize(i.worldNormal); fixed3 worldLightDir normalize(_WorldSpaceLightPos0.xyz); fixed3 diffuse _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir)); fixed3 reflectDir reflect(-worldLightDir, worldNormal); fixed3 viewDir normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz); fixed3 specular _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss); return fixed4(ambient diffuse specular, 1.0); } ENDCG } } FallBack Specular }reflect函数的第一个参数是入射方向注意要取反因为_WorldSpaceLightPos0指向的是光源方向而reflect需要的是从光源指向表面的方向。_Gloss用float存储因为它的范围可能超过fixed的精度。Blinn-Phong 模型把反射方向换成了半程向量h normalize(viewDir worldLightDir)然后用dot(worldNormal, h)代替dot(reflectDir, viewDir)。改动如下fixed3 halfDir normalize(worldLightDir viewDir); fixed3 specular _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(worldNormal, halfDir)), _Gloss);Blinn-Phong 的高光区域比 Phong 更大更柔和在低光泽度下差异尤其明显。材质面板上把_Gloss从 20 调到 200能看到高光点逐渐收缩变亮。6. 常见报错与排查报错一_LightColor0未定义或全黑。检查 Pass 的 Tags 是否写了LightMode ForwardBase。没有这个标签Unity 不会传递光照相关的内置变量。报错二法线方向反了光照结果完全相反。检查法线变换用的是unity_WorldToObject而不是unity_ObjectToWorld。法线需要逆转置矩阵直接用 ObjectToWorld 在非均匀缩放下会出错。推荐直接用UnityObjectToWorldNormal(v.normal)。报错三高光出现在背光面。大概率是reflect的入射方向没取反。reflect(i, n)要求i是从光源指向交点的方向而_WorldSpaceLightPos0是从交点指向光源所以要用-worldLightDir。报错四_Gloss面板调大后高光消失。这是正常的。pow(saturate(dot(...)), _Gloss)中点积结果在[0,1]之间指数越大结果越小。如果高光完全看不见检查_Specular颜色是不是太暗或者光源强度不够。报错五场景里多个光源时结果不对。上面的代码只处理了一个平行光。_WorldSpaceLightPos0在点光源或聚光灯下语义不同需要配合_WorldSpaceLightPos0.w判断光源类型。多光源场景需要额外的 Pass 处理这部分在后续章节展开。7. 验证与对比把四种模型放在一起看建四个胶囊体分别赋上逐顶点漫反射、逐像素漫反射、半兰伯特、Blinn-Phong 高光的材质。场景里放一个平行光调整角度让光从侧上方打过来。逐顶点漫反射的明暗交界有锯齿背光面纯黑。逐像素漫反射锯齿消失但背光面依然黑。半兰伯特背光面有过渡整体更柔和。Blinn-Phong 在向光面出现明显高光_Gloss调到 100 以上时高光收缩成一个亮点。如果你需要快速验证不同参数下的光照公式输出可以用 TaoToken 的模型对话功能跑一下数值对比入口在 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。对于需要长期做 Shader 编码和 Agent 辅助的场景Coding Plan 的入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。最后提一个实际编码中的习惯fixed和float的选择。颜色值、归一化后的标量用fixed就够了范围在[0,1]之间。世界空间坐标、视角方向、需要参与复杂运算的向量用float。_Gloss这种可能超过 1 的参数也用float。精度选低了会出现奇怪的色带选高了浪费寄存器按实际范围来就行。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。