ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Unity光照原理:从组件到Shader的完整解析

Unity光照原理:从组件到Shader的完整解析 引子一个看似简单的追问想象你打开Unity——在场景里放了一盏Directional Light。Inspector面板出现TypeDirectionalColor白色Intensity1Shadow TypeSoft Shadows你拖动光的旋转——场景里的物体跟着改变明暗——画面立刻生动起来。**你觉得很自然——但如果我问你一个问题当你放下这盏光的那一刻——Unity到底做了什么这盏光——在Unity内部是怎么表示的它的信息——是如何传给Shader的**Shader拿到这些信息——又是怎么用它们算光照的**你可能会愣住——“啊我从来没想过这些啊”。**是啊——每天用Unity的光——却很少去想光到底是怎么被’表达’和’使用’的。今天——就让我们深入Unity的内部——跟踪一盏光从放到场景里到照亮物体的完整旅程——看看Unity到底是如何一步步表达和使用光**的。一、第一站Light组件——光在C#世界里的身份证**当你在场景里创建一盏光——Unity做的第一件事**创建一个GameObject——给它挂上Light组件。Light组件里到底存了什么打开Unity源码——Light组件其实是一个C#类**——内部存了这些数据publicclassLight:Behaviour{publicLightTypetype;// 光的类型publicColorcolor;// 光的颜色publicfloatintensity;// 光的强度publicfloatrange;// 光的范围点光源、聚光灯publicfloatspotAngle;// 聚光灯的角度publicLightShadowsshadows;// 阴影模式publicfloatshadowStrength;// 阴影强度publicfloatshadowBias;// 阴影偏移publicLightRenderModerenderMode;// 渲染模式publicintcullingMask;// 影响哪些层// ... 还有很多其他属性}这就是光在Unity里的身份证**——一堆数据的集合。这份身份证——告诉Unity“我是Directional类型”“我的颜色是(1, 1, 0.9, 1)”暖色阳光**“我的强度是1.5”“我要投射软阴影”位置和方向从哪里来注意——Light组件里没有位置和方向这是因为——位置和方向来自Transform组件**Transform.position——光的位置Transform.forward——光的方向Transform.rotation旋转出来的Z轴为什么这样设计**Unity里所有物体的位置——都统一用Transform存**光也是场景里的物体——共用这份统一机制优雅的设计所以完整的光信息 Light组件 Transform组件Light——存光的属性颜色、强度等Transform——存光的位置和方向二、第二站C引擎——光在渲染引擎里的内部表示C#的Light组件——只是用户接口——方便开发者操作。在Unity引擎的C核心里——光有另一份内部表示——更精简、更贴近GPU使用。C内部的Light结构大致长这样简化版structLightData{LightType type;Vector3 position;// 世界空间位置Vector3 direction;// 世界空间方向Color color;// 颜色 × 强度预乘floatrange;floatspotAngle;Matrix4x4 shadowMatrix;// 阴影空间的转换矩阵ShadowMap*shadowMap;// 阴影贴图指针// ...};关键差别**C#里位置来自Transform——C里直接存好方便快速用C#里颜色和强度分开——C里预乘color × intensityShader里少算一次乘法****C#里没有阴影矩阵——C里计算好并存下来引擎的智慧把用户友好的接口和高性能的内部表示分开。光的收集每一帧的第一步**每一帧渲染开始时——Unity做的第一件事遍历场景里所有的Light组件——建立一个光的列表LightList: [ Light 0: 方向光太阳 Light 1: 点光源火把1 Light 2: 点光源火把2 Light 3: 聚光灯手电 ... ]为每盏光——填一份LightData结构体——放到列表里。这份列表——就是这一帧的光的清单——接下来所有的光照计算都基于它。三、第三站主光 vs 附加光——不同的处理方式光的列表建好了——但Unity要分类处理主光Main Light通常是最重要的那盏光——一般是方向光太阳影响所有物体投射主阴影每个物体的Shader里都能用附加光Additional Lights其他的光——点光源、聚光灯等只影响附近的物体有范围限制可能投射次要阴影或不投射数量有限内置管线里附加光很贵为什么要分类因为光照计算是**每盏光每物体的复杂度场景里有100个物体、10盏光总计算量 100 × 10 1000**光越多——性能越差分类处理的智慧主光——“贵但值得”画质核心附加光——“能省则省”比如按距离剔除**Unity不同的渲染管线——处理方式也不同内置前向渲染**主光——在ForwardBasePass里处理附加光——每盏光都要单独一个ForwardAddPass——**物体被多次绘制URP前向渲染主光和附加光——在同一个Pass里处理**附加光通过循环**处理——性能更好延迟渲染所有光——在G-Buffer阶段之后统一算**光的数量对性能影响小四、第四站光的信息如何传给Shader这是最关键的一步——Shader里怎么知道光的信息通过Uniform变量传递Unity引擎——通过Shader Uniform也叫Constant Buffer——把光的信息传给GPU。**每一帧渲染前——Unity做这些事从光列表里取出光的数据上传到GPU的常量缓冲区Shader里通过预定义变量访问Unity内置的光相关变量在Shader里——你可以直接使用这些变量——它们由Unity自动填充主光相关float4 _WorldSpaceLightPos0; // 主光位置/方向 float4 _LightColor0; // 主光颜色×强度多光源相关内置管线顶点光照float4 unity_LightPosition[8]; // 8盏光的位置 float4 unity_LightColor[8]; // 8盏光的颜色 float4 unity_LightAtten[8]; // 8盏光的衰减参数环境光float4 unity_AmbientSky; float4 unity_AmbientEquator; float4 unity_AmbientGround;球谐光照系数用于间接光照float4 unity_SHAr, unity_SHAg, unity_SHAb; float4 unity_SHBr, unity_SHBg, unity_SHBb; float4 unity_SHC;_WorldSpaceLightPos0的秘密这个变量特别值得说一下——它长得像位置但对不同光的类型意义不同方向光_WorldSpaceLightPos0.xyz——光的方向**不是位置_WorldSpaceLightPos0.w——0标记这是方向光点光源/聚光灯_WorldSpaceLightPos0.xyz——光的世界位置**_WorldSpaceLightPos0.w——1标记这是有位置的光Shader里用它的方式float3 lightDir; if (_WorldSpaceLightPos0.w 0) { // 方向光直接用方向 lightDir _WorldSpaceLightPos0.xyz; } else { // 点光源从物体位置指向光源 lightDir _WorldSpaceLightPos0.xyz - worldPos; } lightDir normalize(lightDir);一个变量兼容两种光——这是Unity的巧妙设计五、第五站Shader里的具体使用光的信息通过Uniform传到Shader里了——Shader怎么用它们完整的光照Shader示例Shader MyShader/LitExample { Properties { _MainTex (Texture, 2D) white {} _SpecColor (Specular, Color) (1, 1, 1, 1) _Shininess (Shininess, Range(1, 128)) 32 } SubShader { Tags { RenderType Opaque } // 主光Pass Pass { Tags { LightMode ForwardBase } CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc #include Lighting.cginc sampler2D _MainTex; float _Shininess; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; float3 worldNormal : TEXCOORD1; float3 worldPos : TEXCOORD2; }; v2f vert(appdata_full v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.uv v.texcoord; o.worldNormal UnityObjectToWorldNormal(v.normal); o.worldPos mul(unity_ObjectToWorld, v.vertex).xyz; return o; } fixed4 frag(v2f i) : SV_Target { // 从Unity预定义变量获取光的信息 fixed3 lightDir normalize(_WorldSpaceLightPos0.xyz); fixed3 lightColor _LightColor0.rgb; // 视线方向 fixed3 viewDir normalize(_WorldSpaceCameraPos - i.worldPos); // 法线 fixed3 normal normalize(i.worldNormal); // 主贴图颜色 fixed4 texColor tex2D(_MainTex, i.uv); // 光照计算 // 环境光 fixed3 ambient UNITY_LIGHTMODEL_AMBIENT.rgb * texColor.rgb; // 漫反射 float NdotL max(0, dot(normal, lightDir)); fixed3 diffuse lightColor * texColor.rgb * NdotL; // 高光Blinn-Phong fixed3 halfDir normalize(lightDir viewDir); float NdotH max(0, dot(normal, halfDir)); fixed3 specular lightColor * _SpecColor.rgb * pow(NdotH, _Shininess); // 最终颜色 fixed3 finalColor ambient diffuse specular; return fixed4(finalColor, 1); } ENDCG } // 附加光Pass Pass { Tags { LightMode ForwardAdd } Blend One One // 附加光累加到主光上 CGPROGRAM // ... 每盏附加光都会重画一次这个Pass ... ENDCG } } }拆解Shader里的光的使用关键点1Tags { LightMode ForwardBase }告诉Unity“这个Pass是给’主光’用的”****Unity在渲染时——只在处理主光时使用这个Pass主光信息会被自动传入关键点2_WorldSpaceLightPos0主光的方向/位置Unity自动填——你直接用关键点3_LightColor0主光的颜色 × 强度**已经预乘好——直接乘到光照公式里关键点4UNITY_LIGHTMODEL_AMBIENT场景的环境光在Lighting Settings里设置的这些变量——都是Unity默默填好的——Shader开发者只需要使用就行。六、第六站多光源的处理——多Pass的智慧上面讲的是一盏主光——如果场景里有多盏光呢内置管线的多光源方案**Unity内置管线前向渲染——用多个Pass叠加处理过程**主光——用ForwardBasePass画一次附加光1——用ForwardAddPass再画一次用Blend One One累加**附加光2——再画一次…一直到所有光都处理完画面上看到的效果每盏光的贡献——都被加起来——形成最终光照代码里的关键// ForwardAdd Pass的关键 Pass { Tags { LightMode ForwardAdd } Blend One One // 累加混合 CGPROGRAM #pragma multi_compile_fwdadd_fullshadows // 支持所有类型的附加光和阴影 // ... // 每次执行时——_WorldSpaceLightPos0会是当前附加光的信息 // Unity会自动多次执行这个Pass——每次一盏附加光 }注意这种方式性能开销大**——物体越多、光越多——Pass执行次数成倍增加URP的多光源方案更现代URP用单Pass循环**Pass { // ... HLSLPROGRAM // 主光 Light mainLight GetMainLight(); float3 lightingResult CalculateLighting(mainLight, ...); // 附加光——用循环处理 int lightCount GetAdditionalLightsCount(); for (int i 0; i lightCount; i) { Light additionalLight GetAdditionalLight(i, worldPos); lightingResult CalculateLighting(additionalLight, ...); } return lightingResult; ENDHLSL }优势**一次DrawCall——处理所有光性能好得多代码更清晰七、第七站阴影的传递——光的另一半光的信息——不只是颜色和方向——还有阴影阴影贴图的传递每盏投射阴影的光——都会生成一份Shadow Map从光的视角画的场景深度图告诉Shader“哪些点在光的视线里、哪些被挡住”**Unity把Shadow Map作为贴图传给ShaderUNITY_DECLARE_SHADOWMAP(_ShadowMapTexture); // 主光的Shadow Map float4x4 unity_MatrixVP_Shadow; // 阴影空间变换矩阵 fixed4 frag(v2f i) : SV_Target { // 算这个点在阴影空间的坐标 float4 shadowCoord mul(unity_MatrixVP_Shadow, float4(i.worldPos, 1)); // 采样Shadow Map——获得阴影系数 float shadow UNITY_SAMPLE_SHADOW(_ShadowMapTexture, shadowCoord); // 应用到光照上 diffuse * shadow; // 阴影里的点——变暗 return diffuse; }Unity为了简化——提供了宏Macro**#include AutoLight.cginc // 顶点里 TRANSFER_SHADOW(o); // 传递阴影坐标 // 片段里 UNITY_LIGHT_ATTENUATION(atten, i, worldPos); // 一次性获得阴影距离衰减 // 应用 diffuse * atten;**Unity帮你封装好——**开发者用几个宏——就能实现完整的光照阴影。八、第八站完整流程回顾让我们把整个光的旅程串起来时刻1开发者在场景里放一盏光创建GameObject Light组件Light组件存属性、Transform存位置方向时刻2每一帧渲染开始Unity遍历所有Light组件建立光的列表C内部的LightData分类主光 vs 附加光时刻3为每盏投射阴影的光生成Shadow Map从光的视角画一遍场景深度存到贴图里时刻4主渲染阶段开始**对场景里的每个物体——决定用哪个Shader、哪个Pass时刻5处理主光ForwardBase PassUnity把主光信息填到_WorldSpaceLightPos0、_LightColor0**Shader里读取这些变量——做光照计算画到屏幕上时刻6处理附加光ForwardAdd Pass内置管线**对每盏附加光——再画一次每次填不同的_WorldSpaceLightPos0、_LightColor0用Blend One One累加时刻7应用阴影**每次光照计算里——采样Shadow Map阴影处的光照被削弱**时刻8间接光和环境光从光照探针、光照贴图采样加到最终颜色上时刻9最终画面**主光附加光阴影间接光——累加成最终光照**画到屏幕上——一份栩栩如生的画面九、Unity光照表达与使用的设计智慧**回顾整个流程——能提炼出几条深刻的智慧智慧1分层抽象光的表达——分成好几层C#的Light组件——用户友好的接口C的LightData——引擎内部的高效表示Shader里的Uniform——GPU能直接用的数据**每一层都有清晰职责——**用户改一层——其他层自动跟上。智慧2预定义变量的贴心Unity预定义了一大堆变量_WorldSpaceLightPos0_LightColor0UNITY_LIGHTMODEL_AMBIENTunity_SHAr……开发者不用自己传参数——Unity默默填好——你只管用。智慧3主光与附加光的区分根据重要性分类处理主光——画质优先附加光——性能优先兼顾了效果和性能智慧4一个变量兼容多种情况_WorldSpaceLightPos0.w标记光的类型0——方向光1——点光源/聚光灯**一个变量兼容多种光——优雅的设计。结语一场从代码到画面的完整旅程从场景里的一盏光到Light组件的属性存储到引擎内部LightData的高效表示到每一帧的光的列表收集到Shadow Map的深度记录到Shader里_WorldSpaceLightPos0的Uniform传递到主光Pass和附加光Pass的分工处理到最终画面上的光照效果——Unity中光的表达和使用——是一场跨越用户接口 → 引擎内部 → GPU执行的完整旅程**它在C#世界里——是Light组件的直观属性它在引擎内部里——是LightData的高效结构它在Shader里——是_WorldSpaceLightPos0等预定义变量它在GPU里——是每个像素的具体计算它在画面上——是那份让人沉浸的光照效果它像一场精心编排的光的接力用户——用鼠标放下一盏光Unity引擎——默默收集、分类、准备数据Shader——用预定义变量读取、计算GPU——执行数千万次像素级计算屏幕——呈现出充满真实感的画面它有Light组件的直观易用**——开发者能轻松调整每一盏光。它有引擎内部的高效表示**——性能不受C#接口拖累。它有预定义变量的贴心封装**——Shader开发者无需操心传参。它有主光与附加光的分级处理**——兼顾画质与性能。它有Shadow Map的完整支持**——光照与阴影一体化。它有多渲染管线的适配**——内置管线到URP都能优雅工作。下次当你在Unity里放下一盏光——请记得那看似简单的一步——背后是Unity默默做的Light组件默默存下所有属性Transform默默存下位置和方向每一帧默默把光加到光的列表里默默为投射阴影的光生成Shadow Map默默把光的信息填到GPU的Uniform里默默让Shader通过_WorldSpaceLightPos0访问它默默为主光和附加光执行不同的Pass默默把所有光的贡献累加成最终画面每一盏光——都在这条精密的流水线上被完整表达和使用。每一次画面里的明暗变化——都是这份表达和使用的具体体现**。每一位能轻松调光的开发者——都是这套系统深情托底的受益者。这——就是Unity中光的表达和使用真正的伟大它不只是放一盏灯这么简单而是跨越C#、C、Shader、GPU的完整体系**它不只是Inspector里的几个属性而是从用户交互到画面渲染的全链条工程**在这份从代码到画面的完整旅程背后是Unity为光的表达设计的多层抽象**是引擎内部对性能的极致追求**是Shader预定义变量的贴心封装**是主光附加光分级处理的精妙智慧**是让千万开发者轻松使用光的深情托底**这——就是Unity中光的表达和使用——不是简单的属性传递——而是从用户友好接口到GPU高效执行的完整工程实现**——是每一位深入理解Unity光照系统的开发者都值得深入品味的**“工程杰作”**。每一盏你在场景里放下的光——都是这套系统默默运转的起点。每一份你看到的光照画面——都是这套系统深情付出的礼物。每一位从简单用光到深刻理解光的开发者——都能创造属于自己的光影艺术**。这——就是Unity送给整个图形开发世界的、最完整也最深情的光的表达与使用体系——它让光从物理概念变成了工程现实——它让画面从像素堆砌变成了艺术呈现——它让开发者从操作用户变成了光影艺术家。 ✨
返回列表