当前位置: 首页 > news >正文

Unity3D Shader 法线与基础光照

Unity3D Shader 法线与基础光照

前言

前面的文章里,我们已经反复提到“法线”这个词。

比如:

  • 顶点法线外扩做描边
  • 光照方向和法线方向做点乘
  • 法线贴图会改变表面受光表现

那法线到底是什么?

为什么一旦涉及光照,几乎就一定要用到法线?

这一篇,我们从最基础的角度出发,讲清楚:

  • 法线是什么
  • 光照为什么和法线有关
  • dot点乘在光照里代表什么
  • 如何写一个最基础的 Lambert 漫反射 Shader

什么是法线(Normal)

法线可以简单理解为:

一个表面“朝向哪里”的方向。

它通常是一个单位向量。

例如一个朝上的平面,它的法线可能是:

(0,1,0)

也就是说,这个面朝向 Y 轴正方向。

为什么法线和光照有关

因为光照最基本的问题就是:

光线打到这个表面时,它到底是正面迎光,还是斜着迎光,还是背光?

而判断这件事,最直接的方法就是:

  • 看表面的法线方向
  • 看光照方向

这两个方向越一致,说明表面越正对光源,通常就越亮。

越偏离,通常就越暗。

dot 点乘为什么这么重要

在 Shader 光照中,最常见的一行代码就是:

dot(normal,lightDir)

点乘可以理解成:

两个方向有多接近。

它的结果范围大致是:

  • 1:完全同方向
  • 0:互相垂直
  • -1:完全相反

在光照里通常会写成:

saturate(dot(normal,lightDir))

意思是:

  • 小于 0 的部分直接压成 0
  • 大于 1 的部分压成 1

这样就得到一个 0~1 的受光强度。

最基础的漫反射思想

最经典的基础光照模型之一,就是Lambert 漫反射

它的核心就是:

diffuse=max(0,dot(N,L))

其中:

  • N:法线方向(Normal)
  • L:光照方向(Light Direction)

当表面正对光源时:

  • dot接近 1
  • 物体更亮

当表面转到侧面时:

  • dot接近 0
  • 物体更暗

这就是最基础的“明暗变化”。

在 Shader 中如何拿到法线

Mesh 顶点输入里,通常这样声明:

float3 normal:NORMAL;

例如:

structappdata{float4 vertex:POSITION;float3 normal:NORMAL;};

这表示从模型数据中读取法线。

但要注意:

这里的法线通常是对象空间法线

而做光照时,我们通常更常用世界空间法线

因此往往要转换一下:

float3 worldNormal=UnityObjectToWorldNormal(v.normal);

为什么光照计算常用世界空间

因为:

  • 灯光方向通常在世界空间下更容易理解
  • 摄像机位置是世界空间
  • 物体在场景中的朝向变化,也更方便统一计算

只要参与同一次运算的数据都在同一个空间,结果才是可靠的。

一个最基础的 Lambert 光照 Shader

下面写一个最基础的版本:

  • 支持主方向光
  • 包含环境光 + 漫反射
  • 使用法线和光照方向做点乘
Shader"Custom/SimpleLambert"{Properties{_Color("Main Color",Color)=(1,1,1,1)}SubShader{Pass{Tags{"LightMode"="ForwardBase"}CGPROGRAM#pragmavertex vert#pragmafragment frag#pragmamulti_compile_fwdbase#include"UnityCG.cginc"#include"Lighting.cginc"structappdata{float4 vertex:POSITION;float3 normal:NORMAL;};structv2f{float4 pos:SV_POSITION;float3 worldNormal:TEXCOORD0;};fixed4 _Color;v2fvert(appdata v){v2f o;o.pos=UnityObjectToClipPos(v.vertex);o.worldNormal=UnityObjectToWorldNormal(v.normal);returno;}fixed4frag(v2f i):SV_Target{fixed3 N=normalize(i.worldNormal);fixed3 L=normalize(_WorldSpaceLightPos0.xyz);fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz*_Color.rgb;fixed ndotl=saturate(dot(N,L));fixed3 diffuse=_LightColor0.rgb*_Color.rgb*ndotl;returnfixed4(ambient+diffuse,_Color.a);}ENDCG}}}

代码拆解

1 从模型里读取法线

float3 normal:NORMAL;

2 把法线转到世界空间

o.worldNormal=UnityObjectToWorldNormal(v.normal);

3 获取主光方向

fixed3 L=normalize(_WorldSpaceLightPos0.xyz);

这里我们主要讨论方向光(Directional Light)的基础情况。

4 计算漫反射强度

fixed ndotl=saturate(dot(N,L));

这一步就是核心:

  • 法线越朝向光源 → 值越大
  • 法线越背离光源 → 值越小

5 乘上灯光颜色和材质颜色

fixed3 diffuse=_LightColor0.rgb*_Color.rgb*ndotl;

6 加上一点环境光

fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz*_Color.rgb;

如果没有环境光,背光面会黑得太死。

效果会是什么样

这个 Shader 的效果特点是:

  • 朝向光源的一面更亮
  • 背向光源的一面更暗
  • 比纯色 Unlit 更有体积感

这就是最基础的“受光”感觉。

Half-Lambert:让阴影面别那么黑

标准 Lambert 在某些卡通或风格化场景里,会显得阴影过硬。

一个常见改法是:

floathalfLambert=dot(N,L)*0.5+0.5;

再做一次限制:

halfLambert=saturate(halfLambert);

这样背光面不会一下掉到纯黑,会显得更“柔和”。

很多早期卡通渲染教程都会用这种方法。

法线方向不对会发生什么

如果法线错误,最直接的表现就是:

  • 明暗反了
  • 某些面不该亮却很亮
  • 旋转模型后受光异常

所以当你发现光照不对时,除了看代码,也要怀疑:

  • 模型法线是否正确
  • 空间是否统一
  • 有没有忘记normalize

为什么几乎总要normalize

因为法线和光照方向参与点乘时,我们通常希望比较的是“方向”,而不是长度。

所以会习惯这样写:

fixed3 N=normalize(i.worldNormal);fixed3 L=normalize(_WorldSpaceLightPos0.xyz);

不归一化虽然有时“看起来也能跑”,但结果可能会失真。

常见坑

1 法线和光照方向不在同一个空间

例如:

  • 法线在对象空间
  • 光照方向在世界空间

直接点乘就是错的。

2 忘了归一化

点乘结果会不稳定。

3 以为法线就是顶点位置方向

法线是“表面朝向”,不是顶点坐标。

4 背光面全黑,以为 Shader 写错了

很多时候只是因为你只写了 Lambert,没加环境光。

总结

这一篇最重要的核心只有一句:

基础光照,本质上就是“表面朝向”和“光照方向”的关系。

在 Shader 中最关键的公式就是:

saturate(dot(N,L))

你可以先记住下面这几个点:

  • NORMAL用来接收模型法线
  • 光照计算常用世界空间法线
  • dot(N, L)表示受光强度
  • Lambert是最基础的漫反射模型
  • 环境光可以避免背光面死黑

后面你继续学:

  • Blinn-Phong 高光
  • Fresnel
  • 法线贴图
  • PBR

其实都是在这个基础上不断往上加东西。

http://www.jsqmd.com/news/1304386/

相关文章:

  • 如何高效使用MP4Box.js实现浏览器端MP4文件处理:开发者实战指南
  • TREA框架在安卓开发中的高效实践与应用
  • OpenClaw 2.7.9 第一次启动加载缓慢?底层原因与优化方式分享
  • 基于小马宝莉主题的对话生成工具:角色性格一致性实践指南
  • Hive表结构变更实战:ALTER TABLE核心操作与Schema演化最佳实践
  • 中小律所数字化转型:零代码管理软件的核心优势
  • 开源公益如何通过技术协作创造社会价值
  • Windows平台B站第三方客户端终极指南:免费开源BiliBili-UWP完全使用教程
  • 如何用Moonlight-Switch在Switch上实现PC游戏串流:免费高效的跨平台解决方案
  • 智能车调试:控制策略优化与物理防护的工程实践对比
  • AI生活化产品从概念到上线的完整技术决策全景
  • AI对话应用增长策略:从工具到生态的演进与商业化设计
  • GoWeb 处理请求详解:请求行、请求头、请求参数与给客户端响应
  • 终极直播输入显示指南:Input Overlay 免费插件完整教程
  • Matplotlib色彩系统全解析:从Colormap到Palette的数据可视化进阶指南
  • 开源大模型迎来Claude时刻:GLM-5.2与Mythos深度评测与部署指南
  • Android逆向实战:非ROOT环境下Frida重打包注入完整指南
  • MemSlides:基于记忆增强的AI PPT生成框架解析与应用
  • Nacos微服务实战:从服务发现到配置管理的完整指南
  • 5V转1.8V电源设计:DC-DC与LDO选型、电路设计及调试全解析
  • 可灵视频时长封顶真相曝光:为什么你的45秒作品总被截断?3大底层限频机制深度拆解
  • 13个实用技巧:从根音战士到律动引擎的贝斯编曲指南
  • AI多Agent协作系统实战(二十八):顶栏统一战争——从1个页面异常到31个页面全量对齐
  • GHelper完整使用指南:告别臃肿,华硕笔记本轻量控制方案
  • 2026 年至今,吴桥评价高的钽块上门回收门店哪个好,卖闲置金属还能赚外快?我叫的这上门收料的,连钽块都给当场结钱了 - 鉴选官
  • 2026推荐:遵义行政套房酒店——元本酒店(汇川区古城店)深度体验指南 - 装修教育财税推荐2026
  • AI助手APP竞争新局:从技术秀场到场景渗透,如何构建核心竞争力
  • Windows下Python实现PDF转PPTX:poppler配置与自动化报告生成
  • SpringBoot医院血液管理系统开发实践
  • STM32H743入门实战:从环境搭建到LED与按键控制