You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

技术咨询:如何计算聚光灯遮罩的正确采样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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 10:54:57