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

URP自定义光照Shader实战:完整光照与阴影实现

下面以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中操作:

  1. 创建一个Material;
  2. 将Shader设置为:
Custom/CustomSimpleLit
  1. 将Material赋给Cube、Sphere、Plane等模型;
  2. 确认项目使用URP;
  3. 场景中添加Directional Light;
  4. 在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并参与像素计算的一套完整实现。

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

相关文章:

  • 具身智能协同机制研究:TVA与VLA的统一建模(3)
  • 2026年六西格玛DOE实验设计怎么入门——众智商学院张明老师制造业工艺优化多因子分析实操 - 众智商学院cppm官方
  • CSS box-shadow 盒阴影完全指南:从参数顺序到多层立体投影
  • 基于Coze平台构建短视频自动化生成工作流:从文案到发布的完整实践
  • 青岛厂房空气能热泵销售安装哪家好认准青岛岳峰制冷工程 - 热点品牌推荐
  • C语言编程思维:从语法到实战,构建健壮程序的系统方法
  • Unity PlayerPrefs编辑器:原理、应用与高效调试指南
  • 从AI玩具到工程化:用“配方”思维构建可复现的生成式AI工作流
  • 动图怎么变成视频 2026亲测可用的免费方法 - 图片处理研究员
  • Unity Inspector变量显示控制:public、private与特性Attribute实战指南
  • 2026亲测有效教程:不下载软件怎么把视频转成GIF - 图片处理研究员
  • 腾讯云跨账号服务器迁移实战:镜像共享与零停机部署指南
  • 哈尔滨网站建设制作哪家好:揭秘本地优质服务商的选择逻辑与避坑指南
  • Python读取Chrome历史记录SQLite数据库并导出Excel完整教程
  • Spring Cloud Gateway构建系统边界防护:拦截遗留系统数据污染与能量渗透
  • 数据格式转换工具部署与API集成实践指南
  • 2026年厂房通风降温怎么选?省电空调与负压风机厂家综合评估 - 优质品牌商家
  • 2026免费无水印的视频转GIF工具推荐 亲测好用的方法 - 图片处理研究员
  • 昇腾大模型推理优化:Kthena与Mooncake实现KVCache复用与分布式加速
  • AI应用开发第一步:掌握提示词工程,让大模型成为高效开发助手
  • 河北杂木颗粒厂家一吨多少方?就近选廊坊森耀生物能源有限公司 - 热点品牌推荐
  • Linux Gnome终端无法启动:从配置文件到系统资源的全面排查与修复指南
  • 2026年最新教程:动图转视频用什么小程序好?亲测这款免安装 - 图片处理研究员
  • 武汉代理记账推荐:创航(武汉)信息咨询有限公司一站式财税服务指南 - 行业深度分析
  • 单片机开发实战指南:从51到STM32的系统思维与项目避坑
  • MTK平台闪光灯驱动开发全解析:从HAL到底层硬件控制
  • mysql更新数据与删除数据
  • 用 nano-banana2 批量生图几百张,怎么自动挑出能用的?给生图API 配一套质检
  • 2026萤石球服务商十大实力口碑榜,备婚新人照着选不踩坑 - myqiye
  • 2026年AI核心技能:小模型训练实战指南与产业应用解析