下面以URP(Universal 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("自定义光照颜色")]publicColorlightColor=Color.cyan;[Header("自定义光照方向")]publicVector3lightDirection=newVector3(0.5f,1.0f,0.2f);[Header("自定义光照强度")][Range(0f,10f)]publicfloatintensity=1.0f;privatestaticreadonlyintCustomLightColorID=Shader.PropertyToID("_CustomLightColor");privatestaticreadonlyintCustomLightDirectionID=Shader.PropertyToID("_CustomLightDirection");privatestaticreadonlyintCustomLightIntensityID=Shader.PropertyToID("_CustomLightIntensity");privatevoidUpdate(){// Shader中通常希望拿到“从表面指向光源”的方向。Vector3normalizedDirection=lightDirection.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();例如:
Materialmaterial=GetComponent<Renderer>().material;material.SetColor("_BaseColor",Color.red);material.SetFloat("_Metallic",0.8f);material.SetFloat("_Smoothness",0.9f);但需要注意:
renderer.material会生成材质实例,频繁使用可能增加内存开销。
如果只是给单个Renderer传递不同参数,更推荐使用MaterialPropertyBlock:
usingUnityEngine;publicclassSetObjectLightData:MonoBehaviour{privateRenderertargetRenderer;privateMaterialPropertyBlockpropertyBlock;privatestaticreadonlyintCustomIntensityID=Shader.PropertyToID("_CustomLightIntensity");privatevoidAwake(){targetRenderer=GetComponent<Renderer>();propertyBlock=newMaterialPropertyBlock();}privatevoidUpdate(){targetRenderer.GetPropertyBlock(propertyBlock);floatvalue=Mathf.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并参与像素计算的一套完整实现。