
下面以URPUniversal Render Pipeline为例给出一个可实际使用的自定义光照Shader。这个Shader能够接收Unity自动传入的主光源颜色、方向主光源阴影额外点光源、聚光灯额外灯光阴影环境光 / Light Probe球谐光照材质颜色、主纹理、金属度、光滑度相机位置、物体世界坐标、世界法线。同时也会演示如何从C#向Shader传递自定义光照参数。一、完整URP自定义光照Shader新建文件Assets/Shaders/CustomSimpleLit.shader代码如下Shader Custom/CustomSimpleLit { Properties { [MainTexture] _BaseMap(Base Map, 2D) white {} [MainColor] _BaseColor(Base Color, Color) (1,1,1,1) _Metallic(Metallic, Range(0, 1)) 0 _Smoothness(Smoothness, Range(0, 1)) 0.5 _AmbientIntensity(Custom Ambient Intensity, Range(0, 2)) 1 _CustomLightColor(Custom Extra Light Color, Color) (1,1,1,1) _CustomLightDirection(Custom Extra Light Direction, Vector) (0,1,0,0) _CustomLightIntensity(Custom Extra Light Intensity, Range(0,10)) 0 } SubShader { Tags { RenderPipeline UniversalPipeline RenderType Opaque Queue Geometry } Pass { Name ForwardLit Tags { LightMode UniversalForward } HLSLPROGRAM #pragma vertex vert #pragma fragment frag // 支持主光源阴影包括级联阴影和屏幕空间阴影 #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN // 支持额外灯光点光源、聚光灯等 #pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS // 支持额外灯光阴影 #pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS // 支持软阴影 #pragma multi_compile_fragment _ _SHADOWS_SOFT // 支持雾效 #pragma multi_compile_fog // SRP Batcher通常要求较高Shader Model #pragma target 4.5 // URP基础函数矩阵、坐标转换、纹理采样等 #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl // URP光照函数GetMainLight、GetAdditionalLight、SampleSH等 #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl // // 1. 材质属性Unity会将这些数据传入UnityPerMaterial常量缓冲区 // CBUFFER_START(UnityPerMaterial) float4 _BaseColor; float4 _BaseMap_ST; float _Metallic; float _Smoothness; float _AmbientIntensity; float4 _CustomLightColor; float4 _CustomLightDirection; float _CustomLightIntensity; CBUFFER_END // // 2. 定义纹理和采样器 // TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); // // 3. 顶点着色器输入结构 // struct Attributes { float4 positionOS : POSITION; // 模型顶点位置对象空间 float3 normalOS : NORMAL; // 模型法线对象空间 float2 uv : TEXCOORD0; // 第一套UV }; // // 4. 顶点着色器输出到片元着色器的数据 // struct Varyings { float4 positionCS : SV_POSITION; // 裁剪空间坐标 float2 uv : TEXCOORD0; // UV坐标 float3 positionWS : TEXCOORD1; // 世界空间坐标 float3 normalWS : TEXCOORD2; // 世界空间法线 float4 shadowCoord : TEXCOORD3; // 阴影坐标 half fogFactor : TEXCOORD4; // 雾效因子 }; // // 5. 顶点着色器 // Varyings vert(Attributes input) { Varyings output; // 获取Unity整理好的顶点空间变换结果 VertexPositionInputs positionInputs GetVertexPositionInputs(input.positionOS.xyz); // 获取对象空间法线转换到世界空间后的结果 VertexNormalInputs normalInputs GetVertexNormalInputs(input.normalOS); // 顶点最终用于屏幕绘制的裁剪空间坐标 output.positionCS positionInputs.positionCS; // 世界空间坐标用于计算点光源距离、视线方向、阴影等 output.positionWS positionInputs.positionWS; // 世界空间法线用于NdotL漫反射计算 output.normalWS normalInputs.normalWS; // 主纹理UV变换 output.uv TRANSFORM_TEX(input.uv, _BaseMap); // 从世界坐标转换到主光源阴影贴图坐标 output.shadowCoord TransformWorldToShadowCoord(positionInputs.positionWS); // 计算雾效因子 output.fogFactor ComputeFogFactor(positionInputs.positionCS.z); return output; } // // 6. 简单的Blinn-Phong高光计算 // float3 CalculateSpecular( float3 normalWS, float3 lightDir, float3 viewDir, float3 lightColor, float smoothness, float metallic) { // 半角向量光线方向和视线方向之间的中间方向 float3 halfDir normalize(lightDir viewDir); // 法线与半角向量的夹角 float NdotH saturate(dot(normalWS, halfDir)); // 光滑度越高高光越集中 float specularPower lerp(8.0, 128.0, smoothness); // Blinn-Phong高光 float specularTerm pow(NdotH, specularPower); // 非金属材质一般高光较弱金属材质高光更接近材质本色 float3 specularColor lerp( float3(0.04, 0.04, 0.04), lightColor, metallic ); return specularTerm * specularColor * lightColor; } // // 7. 片元着色器 // half4 frag(Varyings input) : SV_Target { // -------------------------------------------------------- // A. 采样材质贴图 // -------------------------------------------------------- float4 baseMapColor SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv); float3 albedo baseMapColor.rgb * _BaseColor.rgb; // -------------------------------------------------------- // B. 归一化法线 // -------------------------------------------------------- float3 normalWS normalize(input.normalWS); // -------------------------------------------------------- // C. 获取相机位置 // _WorldSpaceCameraPos是Unity自动传入的全局变量 // -------------------------------------------------------- float3 viewDir normalize(_WorldSpaceCameraPos - input.positionWS); // -------------------------------------------------------- // D. 获取主光源数据 // // Unity会根据场景中最重要的方向光自动传递 // mainLight.direction // mainLight.color // mainLight.shadowAttenuation // mainLight.distanceAttenuation // -------------------------------------------------------- Light mainLight GetMainLight(input.shadowCoord); float3 mainLightDir normalize(mainLight.direction); float3 mainLightColor mainLight.color; // 法线与光照方向点积 float NdotL saturate(dot(normalWS, mainLightDir)); // 主光源阴影衰减 // 1表示没有阴影 // 0表示完全处于阴影中 float mainShadow mainLight.shadowAttenuation; // 主光源漫反射 float3 mainDiffuse albedo * mainLightColor * NdotL * mainShadow; // 主光源高光 float3 mainSpecular CalculateSpecular( normalWS, mainLightDir, viewDir, mainLightColor, _Smoothness, _Metallic ) * mainShadow; // -------------------------------------------------------- // E. 计算额外灯光 // // 额外灯光包括 // 点光源 Point Light // 聚光灯 Spot Light // 非主方向光 Directional Light // -------------------------------------------------------- float3 additionalLightColor 0; #if defined(_ADDITIONAL_LIGHTS) uint additionalLightsCount GetAdditionalLightsCount(); for (uint i 0; i additionalLightsCount; i) { // Unity会自动传入该灯光的 // direction // color // distanceAttenuation // shadowAttenuation Light light GetAdditionalLight(i, input.positionWS); float3 lightDir normalize(light.direction); float additionalNdotL saturate(dot(normalWS, lightDir)); // 漫反射 float3 diffuse albedo * light.color * additionalNdotL * light.distanceAttenuation * light.shadowAttenuation; // 高光 float3 specular CalculateSpecular( normalWS, lightDir, viewDir, light.color, _Smoothness, _Metallic ) * light.distanceAttenuation * light.shadowAttenuation; additionalLightColor diffuse specular; } #endif // -------------------------------------------------------- // F. 获取环境光 / Light Probe数据 // // SampleSH读取Unity传入的球谐函数环境光。 // 动态物体通常依赖Light Probe得到这部分间接光。 // -------------------------------------------------------- float3 ambientGI SampleSH(normalWS) * _AmbientIntensity; // 非金属材质保留较多漫反射 // 金属材质更多依赖镜面反射因此降低漫反射。 ambientGI * albedo * (1.0 - _Metallic); // -------------------------------------------------------- // G. 自定义额外光源 // // 这部分数据不是Unity内置Light组件自动提供 // 而是由后面的C#脚本通过Shader.SetGlobalXXX传入。 // -------------------------------------------------------- float3 customLightDir normalize(_CustomLightDirection.xyz); float customNdotL saturate(dot(normalWS, customLightDir)); float3 customLight albedo * _CustomLightColor.rgb * customNdotL * _CustomLightIntensity; // -------------------------------------------------------- // H. 合并最终光照 // -------------------------------------------------------- float3 finalColor mainDiffuse mainSpecular additionalLightColor ambientGI customLight; // 应用Unity雾效 finalColor MixFog(finalColor, input.fogFactor); return half4(finalColor, baseMapColor.a * _BaseColor.a); } ENDHLSL } } }二、这个Shader中Unity到底传了哪些数据上面代码最重要的部分是Light mainLight GetMainLight(input.shadowCoord);这句代码并不是凭空得到灯光数据而是Unity在渲染前已经把主灯光信息上传到GPU中。mainLight通常包含以下内容struct Light { half3 direction; // 从物体表面指向光源的方向 half3 color; // 灯光颜色和强度 float distanceAttenuation; // 距离衰减 half shadowAttenuation; // 阴影衰减 };对于方向光来说mainLight.direction表示太阳光的方向。mainLight.color表示方向光的颜色和强度。mainLight.shadowAttenuation表示阴影强度1.0完全受光 0.5半阴影区域 0.0完全处于阴影中Shader中的主光漫反射计算float NdotL saturate(dot(normalWS, mainLightDir)); float3 mainDiffuse albedo * mainLightColor * NdotL * mainShadow;可以理解为最终亮度 材质颜色 × 灯光颜色 × 表面朝向光源的程度 × 阴影影响如果物体表面正对灯光NdotL接近1表面最亮。如果表面背对灯光NdotL为0表面不接收这盏灯的直接光。如果处于阴影中mainShadow接近0直接光被削弱。三、主光源阴影数据是如何传进来的顶点着色器中有一句output.shadowCoord TransformWorldToShadowCoord(positionInputs.positionWS);这句代码的作用是把物体顶点的世界坐标转换到“光源视角下的阴影贴图坐标”。Unity会先从光源角度渲染一张深度图也就是Shadow Map。可以把它理解成光源拍摄的一张“黑白深度照片”离光源近的物体深度较小 离光源远的物体深度较大 被遮挡的位置在深度比较时判定为阴影片元着色器中Light mainLight GetMainLight(input.shadowCoord);URP会用input.shadowCoord去采样Shadow Map并得到mainLight.shadowAttenuation因此Shader本身通常不需要手动写大量阴影比较代码URP已经封装好了。四、额外点光源和聚光灯是如何传入Shader的下面代码用于读取额外灯光uint additionalLightsCount GetAdditionalLightsCount(); for (uint i 0; i additionalLightsCount; i) { Light light GetAdditionalLight(i, input.positionWS); }Unity会在渲染前筛选出影响当前物体的额外灯光并将它们上传到GPU缓冲区。每盏灯包含灯光颜色 灯光方向 灯光位置相关信息 距离衰减 聚光灯锥形衰减 阴影衰减。例如点光源离物体较远时light.distanceAttenuation会越来越小。最终点光源的照明结果float3 diffuse albedo * light.color * additionalNdotL * light.distanceAttenuation * light.shadowAttenuation;这意味着点光源会同时受到以下因素影响材质颜色 灯光颜色 表面朝向 距离 阴影。五、环境光和Light Probe数据如何传进Shader代码中的float3 ambientGI SampleSH(normalWS);用于读取Unity传入的球谐函数环境光数据。Unity会将环境光、Light Probe等信息编码成一组SH参数。Shader通过法线方向计算当前表面接收到的环境光。例如朝上的表面可能更多接收天空颜色朝下的表面可能更多接收地面反射颜色在室内时可能受到墙壁和烘焙间接光影响。代码float3 ambientGI SampleSH(normalWS) * _AmbientIntensity; ambientGI * albedo * (1.0 - _Metallic);这里让非金属材质接受更多环境漫反射而金属材质的漫反射较弱。六、通过C#向Shader传递自定义光照数据Unity内置灯光可以通过GetMainLight()和GetAdditionalLight()读取。但有时我们需要自己传递额外数据例如游戏技能范围光特殊月光角色附近的魔法光全局天气颜色自定义昼夜系统太阳光场景全局闪电效果。下面给出一个C#脚本通过全局Shader变量传递一盏自定义方向光。新建脚本Assets/Scripts/CustomGlobalLight.cs代码如下usingUnityEngine;publicclassCustomGlobalLight:MonoBehaviour{[Header(自定义光照颜色)]publicColorlightColorColor.cyan;[Header(自定义光照方向)]publicVector3lightDirectionnewVector3(0.5f,1.0f,0.2f);[Header(自定义光照强度)][Range(0f,10f)]publicfloatintensity1.0f;privatestaticreadonlyintCustomLightColorIDShader.PropertyToID(_CustomLightColor);privatestaticreadonlyintCustomLightDirectionIDShader.PropertyToID(_CustomLightDirection);privatestaticreadonlyintCustomLightIntensityIDShader.PropertyToID(_CustomLightIntensity);privatevoidUpdate(){// Shader中通常希望拿到“从表面指向光源”的方向。Vector3normalizedDirectionlightDirection.normalized;Shader.SetGlobalColor(CustomLightColorID,lightColor);Shader.SetGlobalVector(CustomLightDirectionID,newVector4(normalizedDirection.x,normalizedDirection.y,normalizedDirection.z,0f));Shader.SetGlobalFloat(CustomLightIntensityID,intensity);}}将这个脚本挂到场景任意物体上例如创建一个空物体GameObject └── CustomGlobalLightController └── CustomGlobalLight.cs然后在Inspector中调整Light Color青色、红色、蓝色等 Light Direction光线方向 Intensity光照强度Shader中接收的变量是float4 _CustomLightColor; float4 _CustomLightDirection; float _CustomLightIntensity;对应计算代码float3 customLightDir normalize(_CustomLightDirection.xyz); float customNdotL saturate(dot(normalWS, customLightDir)); float3 customLight albedo * _CustomLightColor.rgb * customNdotL * _CustomLightIntensity;这套流程就是C#脚本 → Shader.SetGlobalColor / SetGlobalVector / SetGlobalFloat → Unity上传GPU全局变量 → Shader读取变量 → 参与最终光照计算七、为材质创建并使用这个Shader在Unity中操作创建一个Material将Shader设置为Custom/CustomSimpleLit将Material赋给Cube、Sphere、Plane等模型确认项目使用URP场景中添加Directional Light在URP Asset中开启Main Light Cast Shadows Additional Lights Additional Light Shadows如果需要额外灯光效果可在场景中添加Point Light Spot Light如果需要看到环境光效果可设置Window → Rendering → Lighting → Environment或在场景中放置Light Probe Group Reflection Probe八、Unity自动传递数据与手动传递数据的区别1. Unity自动传递的数据例如_WorldSpaceCameraPos表示相机世界坐标。GetMainLight()获取主光源数据。GetAdditionalLight()获取额外灯光数据。SampleSH()获取环境光或Light Probe数据。TransformWorldToShadowCoord()计算阴影贴图采样坐标。这些数据由URP渲染管线负责上传通常不需要自己在C#中设置。2. 开发者手动传递的数据例如_CustomLightColor _CustomLightDirection _CustomLightIntensity这些是我们自定义的变量需要自己通过C#设置Shader.SetGlobalColor();Shader.SetGlobalVector();Shader.SetGlobalFloat();如果数据只属于某个材质也可以使用material.SetColor();material.SetVector();material.SetFloat();例如MaterialmaterialGetComponentRenderer().material;material.SetColor(_BaseColor,Color.red);material.SetFloat(_Metallic,0.8f);material.SetFloat(_Smoothness,0.9f);但需要注意renderer.material会生成材质实例频繁使用可能增加内存开销。如果只是给单个Renderer传递不同参数更推荐使用MaterialPropertyBlockusingUnityEngine;publicclassSetObjectLightData:MonoBehaviour{privateRenderertargetRenderer;privateMaterialPropertyBlockpropertyBlock;privatestaticreadonlyintCustomIntensityIDShader.PropertyToID(_CustomLightIntensity);privatevoidAwake(){targetRendererGetComponentRenderer();propertyBlocknewMaterialPropertyBlock();}privatevoidUpdate(){targetRenderer.GetPropertyBlock(propertyBlock);floatvalueMathf.PingPong(Time.time,2.0f);propertyBlock.SetFloat(CustomIntensityID,value);targetRenderer.SetPropertyBlock(propertyBlock);}}这种方式可以让每个物体拥有不同参数但不需要复制出大量材质实例。九、最终的数据传递流程总结以一个像素为例完整过程如下场景中的Directional Light、Point Light、Spot Light ↓ URP收集当前相机可见物体和影响它们的灯光 ↓ Unity将灯光颜色、方向、位置、阴影、距离衰减等上传GPU ↓ Shader顶点着色器计算 世界坐标、世界法线、阴影坐标、UV ↓ Shader片元着色器读取 主光、额外光、阴影、环境光、材质贴图 ↓ 计算漫反射、高光、间接光、阴影 ↓ 输出最终像素颜色最终颜色可以概括为FinalColor 主光漫反射 主光高光 额外灯光 环境光 自定义光照 自发光;也就是本Shader中的float3 finalColor mainDiffuse mainSpecular additionalLightColor ambientGI customLight;这就是Unity中光照数据从场景、C#、渲染管线到最终进入Shader并参与像素计算的一套完整实现。