技术咨询:如何计算聚光灯遮罩的正确采样UV?
问题:聚光灯遮罩纹理的正确UV采样方式
我已经实现了带聚光灯的Blinn-Phong光照计算,核心代码如下:
void BlinnPhong( inout float3 outputColor, PixelMaterial material, float3 lightColor, float3 lightDir, float3 viewDirection, float attenuation) { const float diffuseFactor = dotMax(material.normal, lightDir); outputColor += lightColor * diffuseFactor * material.baseColor * attenuation; const float3 halfwayDirection = normalize(lightDir + viewDirection); float specularFactor = pow(dotMax(material.normal, halfwayDirection), material.roughness); if (diffuseFactor == 0.0f) specularFactor = 0.0f; outputColor += lightColor * specularFactor * material.metalness * attenuation; } void EvaluateSpotLights(inout float3 outputColor, PixelMaterial material, float3 pos, float3 viewDirection) { for (uint i = 0; i < spotLights.Length; ++i) { SpotLight light = spotLights[i]; const float3 posToLight = light.position - pos; const float3 lightDir = normalize(posToLight); //light.direction已归一化 const float theta = dotMax(lightDir, light.direction); const float epsilon = (light.innerOuterCutOff.x - light.innerOuterCutOff.y); const float intensity = saturate((theta - light.innerOuterCutOff.x) / epsilon); const float distance = length(posToLight); const float attenuation = (1.0f / (light.constnatLinearQuadratic.x + light.constnatLinearQuadratic.y * distance + light.constnatLinearQuadratic.z * (distance * distance))) * intensity; BlinnPhong(outputColor, material, light.color, lightDir, viewDirection, attenuation); } }
当前聚光灯效果正常,我希望给聚光灯添加遮罩纹理(已准备两张测试遮罩图,分别是中心亮边缘暗的圆形遮罩、带镂空图案的遮罩)。
我已经修改了BlinnPhong函数,添加了mask参数并将其与attenuation相乘,修改后代码:
void BlinnPhong( inout float3 outputColor, PixelMaterial material, float3 lightColor, float3 lightDir, float3 viewDirection, float attenuation, float mask) { const float diffuseFactor = dotMax(material.normal, lightDir); outputColor += lightColor * diffuseFactor * material.baseColor * attenuation * mask; const float3 halfwayDirection = normalize(lightDir + viewDirection); float specularFactor = pow(dotMax(material.normal, halfwayDirection), material.roughness); if (diffuseFactor == 0.0f) specularFactor = 0.0f; outputColor += lightColor * specularFactor * material.metalness * attenuation * mask; }
现在的问题是:应该使用何种UV来正确采样聚光灯遮罩纹理?
我尝试过以下方案,但都没有得到正确效果:
- 使用网格顶点UV:
//像素着色器中 float mask = t_spotlightMask.Sample(g_pointWrap, input.mesh_uv).r;
- 将
lightDir转换为UV:
float theta = acos(lightDir.y); float phi = atan2(lightDir.z, lightDir.x); float u = phi / (2 * PI) + 0.5; float v = theta / PI;
编辑补充:尝试基于clipPosition计算UV后有进展,但仍存在异常(红色标记区域出现拉伸/错位),对应代码:
float maskSize = 512; float2 scale = 1.0f / float2(maskSize * 0.5f, maskSize * 0.5f); float2 offset = float2(0.0f, 0.5f); float2 uv = clipPosition.xy * scale + offset; float mask = t_spotlightMask.Sample(g_pointWrap, uv).r;
解决方案
要正确采样聚光灯遮罩纹理,核心是从聚光灯的视角将世界空间的像素位置投影到遮罩纹理的UV空间,模拟聚光灯作为"相机"对场景的投影。具体步骤如下:
1. 构建聚光灯的视图-投影矩阵
在CPU端为每个聚光灯计算对应的视图矩阵和投影矩阵,合并后传入着色器:
- 视图矩阵:将世界空间转换为聚光灯局部空间(以聚光灯位置为原点,朝向为-z轴)。
- 投影矩阵:用透视投影匹配聚光灯的内外 cutoff 角度(FOV设为外cutoff角度的2倍,因为FOV是全角)。
示例逻辑(C#):
// 聚光灯视图矩阵 Matrix4x4 viewMatrix = Matrix4x4.LookAt(lightPosition, lightPosition + lightDirection, Vector3.up); // 聚光灯投影矩阵:FOV为外cutoff角度的2倍,宽高比1:1,近远裁剪面按需设置 float fov = 2 * Mathf.Deg2Rad * outerCutoffAngle; Matrix4x4 projMatrix = Matrix4x4.Perspective(fov, 1.0f, 0.1f, lightMaxDistance); // 合并为VP矩阵 Matrix4x4 lightVP = projMatrix * viewMatrix;
2. 在像素着色器中计算遮罩UV
在EvaluateSpotLights函数中,基于聚光灯VP矩阵计算UV:
void EvaluateSpotLights(inout float3 outputColor, PixelMaterial material, float3 worldPos, float3 viewDirection) { for (uint i = 0; i < spotLights.Length; ++i) { SpotLight light = spotLights[i]; // 原有计算lightDir、intensity、attenuation的代码... // 计算遮罩UV float4 clipPos = mul(float4(worldPos, 1.0f), light.lightVP); float2 ndc = clipPos.xy / clipPos.w; // 齐次除法得到NDC范围[-1,1] float2 maskUV = ndc * 0.5f + 0.5f; // 转换为UV范围[0,1] // 采样遮罩纹理,用clamp避免超出范围的无效采样 float mask = t_spotlightMask.Sample(g_clampWrap, maskUV).r; // 让遮罩仅在聚光灯有效范围内生效 mask *= intensity; BlinnPhong(outputColor, material, light.color, lightDir, viewDirection, attenuation, mask); } }
3. 修复之前的异常问题
你之前使用的clipPosition是相机的裁剪空间坐标,不是聚光灯视角的,因此会出现错位。改用聚光灯自身的VP矩阵计算,就能得到正确的投影UV,解决红色标记区域的拉伸/错位问题。
额外优化
- 可以添加范围判断:如果
distance(maskUV - 0.5f, 0.5f) > 0.5f,直接将mask设为0,避免采样遮罩纹理的无效区域。 - 若遮罩需要支持旋转/缩放,可以在VP矩阵后额外添加UV变换矩阵,灵活调整遮罩的显示效果。
内容的提问来源于stack exchange,提问作者Shout
相关产品推荐
相关产品推荐

