如何在unity的前向渲染路径ForwardBase中同时使用逐顶点和逐像素光源

本文介绍了如何在Unity中使用Shade4PointLights函数进行逐顶点光照计算,以解决当光源设置为notimportant时的渲染问题。通过在forwardbase Pass的vert阶段调用该函数,计算点光源的漫反射效果。虽然缺乏高光项,但这种方法适用于多光源场景,尤其是在不追求高精度光效的情况下。文章还探讨了当前版本Unity对顶点光照的限制,并推荐在additional Pass中进行逐像素计算或使用延迟渲染路径。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言:问题来自于《Unity Shader入门精要》中,第195页。书中给出了如何在forwardbase和forwardadd中计算逐像素光照,并没有给出如何同时进行逐顶点的光照,因此当我们把点光源设置为not important时,书中的shader将无法正确渲染此光源。为了解决这个问题我们可以使用9.1.1节中提到的Shade4PointLights函数和一些内置的变量。


1.Shade4PointLights()的用法

float3 Shade4PointLights (
    float4 lightPosX, float4 lightPosY, float4 lightPosZ,
    float3 lightColor0, float3 lightColor1, float3 lightColor2, float3 lightColor3,
    float4 lightAttenSq,
    float3 pos, float3 normal)
{
    // to light vectors
    float4 toLightX = lightPosX - pos.x;
    float4 toLightY = lightPosY - pos.y;
    float4 toLightZ = lightPosZ - pos.z;
    // squared lengths
    float4 lengthSq = 0;
    lengthSq += toLightX * toLightX;
    lengthSq += toLightY * toLightY;
    lengthSq += toLightZ * toLightZ;
    // don't produce NaNs if some vertex position overlaps with the light
    lengthSq = max(lengthSq, 0.000001);
 
    // NdotL
    float4 ndotl = 0;
    ndotl += toLightX * normal.x;
    ndotl += toLightY * normal.y;
    ndotl += toLightZ * normal.z;
    // correct NdotL
    float4 corr = rsqrt(lengthSq);
    ndotl = max (float4(0,0,0,0), ndotl * corr);
    // attenuation
    float4 atten = 1.0 / (1.0 + lengthSq * lightAttenSq);
    float4 diff = ndotl * atten;
    // final color
    float3 col = 0;
    col += lightColor0 * diff.x;
    col += lightColor1 * diff.y;
    col += lightColor2 * diff.z;
    col += lightColor3 * diff.w;
    return col;
}

以上是Shade4PointLights()函数的完整实现代码,可以看到参数列表有足足10个,这些参数也都是UnityCG.cginc中找到的:

unity_4LightPosX0: float4,Unity内置变量,四个分量分别存储着四个光源位置的X坐标

unity_4LightPosY0: float4,Unity内置变量,四个分量分别存储着四个光源位置的Y坐标

unity_4LightPosZ0: float4,Unity内置变量,四个分量分别存储着四个光源位置的Z坐标

unity_LightColor[0].rgb: Unity内置变量,储存着第一个非重要光源的颜色

unity_LightColor[1·].rgb: Unity内置变量,储存着第二个非重要光源的颜色

unity_LightColor[2].rgb: Unity内置变量,储存着第三个非重要光源的颜色

unity_LightColor[3].rgb: Unity内置变量,储存着第四个非重要光源的颜色

unity_4LightAtten0: float4, Unity内置变量,四个分量分别储存着四个光源的光照衰减因子

pos:顶点世界坐标

normal:顶点法线

关于此函数的更详细内容可以参考Shade4PointLights_zengjunjie59的博客-CSDN博客

2.如何在forwardbase中使用此函数来渲染not important的光源

先给出源码:

Shader "Unity Shaders Book/Chapter 9/Forward Rendering" {
	Properties {
		_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
		_Specular ("Specular", Color) = (1, 1, 1, 1)
		_Gloss ("Gloss", Range(8.0, 256)) = 20
	}
	SubShader {
		Tags { "RenderType"="Opaque" }
		
		Pass {
			// Pass for ambient light & first pixel light (directional light)
			Tags { "LightMode"="ForwardBase" }

			//Blend One One
			
			CGPROGRAM
			
			// Apparently need to add this declaration 
			#pragma multi_compile_fwdbase	
			
			#pragma vertex vert
			#pragma fragment frag
			
			#include "Lighting.cginc"
			#include "UnityCG.cginc"
			
			fixed4 _Diffuse;
			fixed4 _Specular;
			float _Gloss;
			
			struct a2v {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
			};
			
			struct v2f {
				float4 pos : SV_POSITION;
				fixed4 color : COLOR;
				float3 worldNormal : TEXCOORD0;
				float3 worldPos : TEXCOORD1;
			};
			
			v2f vert(a2v v) {
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

				fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(o.worldPos));
				fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(o.worldPos));

				//计算逐顶点光照
				float3 fourPointLightColor = Shade4PointLights(unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0, unity_LightColor[0].rgb,
					unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb, unity_4LightAtten0, o.worldPos, o.worldNormal);

				
				o.color = fixed4( fourPointLightColor, 1.0);
				
				return o;
			}
			
			fixed4 frag(v2f i) : SV_Target {
				fixed3 worldNormal = normalize(i.worldNormal);
				fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
				
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
				
			 	fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));

			 	fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
			 	fixed3 halfDir = normalize(worldLightDir + viewDir);
			 	fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

				fixed atten = 1.0;
				
				return fixed4(ambient + (diffuse + specular) * atten+i.color, 1.0);
			}
			
			ENDCG
		}
	
		Pass {
			// Pass for other pixel lights
			Tags { "LightMode"="ForwardAdd" }
			
			Blend One One
		
			CGPROGRAM
			
			// Apparently need to add this declaration
			#pragma multi_compile_fwdadd
			
			#pragma vertex vert
			#pragma fragment frag
			
			#include "Lighting.cginc"
			#include "AutoLight.cginc"
			
			fixed4 _Diffuse;
			fixed4 _Specular;
			float _Gloss;
			
			struct a2v {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
			};
			
			struct v2f {
				float4 pos : SV_POSITION;
				float3 worldNormal : TEXCOORD0;
				float3 worldPos : TEXCOORD1;
			};
			
			v2f vert(a2v v) {
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
				
				return o;
			}
			
			fixed4 frag(v2f i) : SV_Target {
				fixed3 worldNormal = normalize(i.worldNormal);
				#ifdef USING_DIRECTIONAL_LIGHT
					fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
				#else
					fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
				#endif
				
				fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));
				
				fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
				fixed3 halfDir = normalize(worldLightDir + viewDir);
				fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
				
				#ifdef USING_DIRECTIONAL_LIGHT
					fixed atten = 1.0;
				#else
					#if defined (POINT)
				        float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)).xyz;
				        fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
				    #elif defined (SPOT)
				        float4 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1));
				        fixed atten = (lightCoord.z > 0) * tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w * tex2D(_LightTextureB0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
				    #else
				        fixed atten = 1.0;
				    #endif
				#endif

				return fixed4((diffuse + specular) * atten, 1.0);
			}
			
			ENDCG
		}
	}
	FallBack "Specular"
}

可以对比书中给出的shader,需要修改的部分只有forwardbase的vert而已(注意要在v2f中声明fixed4 color:COLOR):

v2f vert(a2v v) {
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

				fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(o.worldPos));
				fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(o.worldPos));

				//计算逐顶点光照
				float3 fourPointLightColor = Shade4PointLights(unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0, unity_LightColor[0].rgb,
					unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb, unity_4LightAtten0, o.worldPos, o.worldNormal);

				
				o.color = fixed4( fourPointLightColor, 1.0);
				
				return o;
			}

保存shader之后,把光源的Render Mode设置为Not Important,就可以看到效果了:

可以在frameDebugger中看到,只调用了一次draw mesh capsule,4个点光源都在forwardbase这个通道中就完成了渲染:

3.存在的问题

第一个很显然的问题是,这个函数计算出的光照结果只有漫反射效果,也就是说没有specular项,对比一下在additional中逐像素的计算结果就可以清晰的看出来:


总结一下:似乎unity现在的版本不太提倡在vert中进行光照计算了,能做的也仅仅是计算点光源的漫反射而已(高光的逐顶点效果实在太差了,内置的模型精度太低)。因此在渲染多光源场景的时候,最好在additional中进行逐像素计算,或者可以使用延迟渲染路径。

如果有错误,欢迎指出,有任何建议的也欢迎进行交流学习,栓Q~

### 像素光源片元光源的区别及实现方式 #### 1. 基本概念 像素光源(Per-Pixel Lighting)片元光源(Per-Fragment Lighting)实际上是同一种光照计算方法的不同称呼。在现代图形学中,“片元”更贴近GPU的实际工作流程,因为最终的光照计算是在片段着色器阶段完成的。 顶点光源(Per-Vertex Lighting)则是另一种光照计算方法,在这种模式下,光照会在顶点着色器阶段被计算并插值到片段着色器阶段[^3]。 --- #### 2. 工作原理对比 ##### (1)像素/片元光源 在这种模式下,光照计算发生在片段着色器中,这意味着每个屏幕上的像素都会独立地进行完整的光照计算。这种方法的优点是可以获得更高的精度更好的视觉效果,尤其是当表面具有复杂的法线贴图或其他细节时[^5]。 优点: - 更高的真实感。 - 支持法线贴图其他高级纹理效果。 缺点: - 计算成本较高,尤其是在有多个动态光源的情况下。 - 对低端设备的压力较大。 ##### (2)顶点光源 在此模式下,光照仅在顶点着色器中计算,并通过插值得到片段的颜色。由于插值的本质特性,某些情况下可能会导致光照不均匀或失真现象。 优点: - 性能开销较低。 - 实现简单,适合静态场景或低多边形模型。 缺点: - 受限于几何体的分辨率,可能导致光照过渡不够平滑。 - 不支持高精度的法线贴图等复杂效果。 --- #### 3. Unity 中的具体实现 ##### (1)像素光源 以下是基于 Lambert 模型的一个简单的像素光照 Shader 示例: ```hlsl Shader "Custom/PerPixelLambert" { Properties { _Diffuse ("Diffuse Color", Color) = (1,1,1,1) } SubShader { Pass { Tags { "LightMode"="ForwardBase" } CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "Lighting.cginc" fixed4 _Diffuse; struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 pos : SV_POSITION; float3 worldNormal : TEXCOORD0; float3 worldPos : TEXCOORD1; }; v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; return o; } fixed4 frag(v2f i) : SV_Target { fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; fixed3 worldNormal = normalize(i.worldNormal); fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz); fixed3 diffuse = max(dot(worldNormal, lightDir), 0) * _LightColor0.rgb * _Diffuse.rgb; fixed3 color = ambient + diffuse; return fixed4(color, 1.0); } ENDCG } } } ``` 上述代码展示了如何在一个片段级别应用漫反射光照模型。 ##### (2)顶点光源 下面是一个顶点光照的示例 Shader: ```hlsl Shader "Custom/PerVertexLambert" { Properties { _Diffuse ("Diffuse Color", Color) = (1,1,1,1) } SubShader { Pass { Tags { "LightMode"="ForwardBase" } CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "Lighting.cginc" fixed4 _Diffuse; struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 pos : SV_POSITION; fixed3 color : COLOR0; }; v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; fixed3 worldNormal = normalize(mul((float3x3)unity_WorldToObject, v.normal)); fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz); fixed3 diffuse = max(dot(worldNormal, worldLightDir), 0) * _LightColor0.rgb * _Diffuse.rgb; o.color = ambient + diffuse; return o; } fixed4 frag(v2f i) : SV_Target { return fixed4(i.color, 1.0); } ENDCG } } } ``` 此代码实现了基本的顶点光照逻辑,其中光照计算完全由顶点着色器负责。 --- #### 4. 使用建议 对于性能敏感的应用程序,可以选择顶点光源;而对于追求高质量渲染效果的游戏或演示,则更适合采用像素光源[^2]。此外,还可以结合硬件能力调整策略,例如利用延迟渲染或多通道渲染技术来平衡质量效率[^4]。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值