Skip to main content

基于Qt 6.11的轻量级实时体积光方案

Comments

基于真实 IES 配光文件构建体积光柱(Volumetric Light Shafts)

傍晚薄雾中,路灯投射出独特的光影图案——这种视觉感受令人着迷。为了用代码还原它,笔者基于 Qt 6.11 的图形能力构建了一套实时体积光(Volumetric Lighting)系统,借助其 QML 着色器集成与 3D 渲染流水线,结合真实 IES 配光文件加以实现。


IES_profile_test_Qt_Hackathon_January_2026a傍晚薄雾中的路灯光影图案

 

最终成果是一套能够实时渲染大气光柱、呈现真实光照分布规律的系统。这是笔者在 2026 年 1 月 Qt Hackathon 中的参赛项目,整个探索过程涉及光传输原理与着色器编程,因此希望在此与大家分享其实现思路。

从单次散射(Single Scattering)出发

光线穿过雾气或尘埃时,会与空气中的粒子发生碰撞并产生散射。理想情况下,应当模拟每一次碰撞——光线击中粒子、改变方向、再击中另一粒子,如此往复。这种方式称为多次散射(Multiple Scattering),效果精美但计算开销极高。

单次散射(Single Scattering)是更简洁的替代方案:只考虑光线与粒子发生一次碰撞后直接到达观察者眼睛的情形。实践证明,这一方案效果出人意料地好,尤其适用于薄雾或大气朦胧效果。

SingleScatterDiagram

单次散射示意图:光线仅与粒子发生一次碰撞,随后直接到达观察者

 

背后的数学推导其实相当直接:对于视线所指向的任意点,只需确定三个量——该点距光源多远、该方向上的光照强度(IES 配光文件的作用正在于此)、以及光路中的雾气密度。三者相乘,即可得到散射强度。

IES 配光文件是什么?

IES 文件是一种人类可读的文本格式,用于存储灯具在各个方向上的实测光照强度,通常利用对称性压缩数据体积。

为使其适配 GPU 处理,笔者在 CPU 端完成文件解析,将其展开为覆盖完整球面方向的 360 × 181 二维图像,并存储于 3D 纹理中。每个纹理层可容纳一个独立的 IES 配光文件,QML 中的着色器可直接采样。这得益于 Qt 的可定制纹理数据机制

运行时无需额外处理对称逻辑,直接进行纹理查询即可。由于 GPU 硬件插值可在实测角度之间平滑过渡,这一方案非常适合用纹理采样实现。

 

ies_texture_slice_002

解析后的 IES 数据:水平与垂直方向均以1°展开,每个纹理像素(Texel)对应一个角度方向上的光强值

核心实现:体积散射着色器(Volumetric Scattering Shader)

这是整个系统最有意思的部分。对于每个像素,需要计算沿视线方向散射进摄像机的光照量。以下是笔者最终采用的实现方案:

float volumetricScatter(vec3 cameraPos, vec3 worldPos, vec3 lightPos, vec3 lightDir,
                        float density, float iesIndex, float omnidirectionality) {
    vec3 rayDir = worldPos - cameraPos;
    float rayLen = length(rayDir);
    rayDir /= rayLen;
    
    // Find the point on our view ray that's closest to the light
    float t = clamp(dot(lightPos - cameraPos, rayDir), 0.0, rayLen);
    vec3 closest = cameraPos + rayDir * t;
    float dist = length(lightPos - closest);
    vec3 lightToPoint = normalize(closest - lightPos);
    
    // Check the IES profile to see how much light goes this direction
    float directionality = min(sampleIES(lightToPoint, lightDir, iesIndex) + omnidirectionality,
    	1.0);
    
    // Classic inverse square falloff with density and ray length
    return (directionality * density * rayLen / (dist * dist + 0.0001));
}

实现的巧妙之处在于最近点的求取方式。与射线步进(Ray Marching)——即沿视线射线逐步采样——不同,这里仅需找到视线上距光源最近的一个点。这是一种近似,但速度快,且实际效果出色的计算方法。

平方反比定律负责处理距离衰减——光线传播时由于发散,强度会随距离的平方递减。乘以射线长度是因为更长的射线会积累更多散射;乘以密度参数则用于控制雾气的厚薄程度。

借助 3D 纹理的 2D 切片,IES 光照强度采样同样直观易用。

float sampleIES(vec3 lightDir, vec3 lightForward, float iesIndex) {
    // Vertical angle (theta): angle from the forward direction
    float theta = acos(dot(lightDir, lightForward));
    
    // Build tangent space for horizontal angle calculation
    vec3 right = normalize(cross(lightForward, vec3(0.0, 0.0, 1.0)));
    vec3 up = cross(right, lightForward);
    
    // Project direction onto a plane perpendicular to the forward
    vec3 projectedDir = lightDir - lightForward * dot(lightDir, lightForward);
    
    // Horizontal angle (phi): rotation around forward axis
    float phi = atan(dot(projectedDir, up), dot(projectedDir, right));
    
    // Convert to UV coordinates [0,1]
    vec2 uv = vec2(phi / (2.0 * PI), theta / PI);
    
    // Sample 3D texture (multiple IES profiles)
    return textureLod(lightIES, vec3(uv, iesIndex / iesCount), 0).r;
}

投影光照与体积光照的融合

该系统同时渲染表面光照(投影光照)与体积效果:

// The fog effect
float scatter = volumetricScatter(cameraPosition, worldPos, lightPosition, lightForward,
                                  scatterDensity, iesIndex, omnidirectionality);
vec3 volumetricLight = lightColor * lightIntensity * scatter;

// Regular surface lighting with the same IES profile
float iesIntensity = min(sampleIES(lightToSurface, lightForward, iesIndex) + omnidirectionality,
	1.0);
float attenuation = 1.0 / (distanceToLight * distanceToLight + 0.0001);
vec3 projectedLight = lightColor * iesIntensity * attenuation * projectionIntensity;

// Add them up
finalColor += (projectedLight + volumetricLight) * distanceAttenuation;

最终效果是:表面上呈现清晰的投影光照图案,空气中则有柔和的体积光晕。两者均遵循相同的 IES 分布规律,因此视觉上高度统一。

 

Screenshot 2026-01-29 130022-1投影光照图案与体积光晕的叠加效果

多光源管理

系统需要同时处理多种光源——路灯、交通灯、车辆大灯,每种光源都有各自的 IES 配光文件与属性参数。笔者采用的方案是:通过简单的球形包围盒对摄像机进行可见性判断,将所有可见(激活中)光源数据打包为纹理,并在每帧更新:

tempLights.push({
    position: Qt.vector3d(x, y, z),
    forward: Qt.vector3d(dx, dy, dz),
    omnidirectionality: 0.0,
    color: "lightblue",
    intensity: 20,
    scatterDensity: 0.4,
    projectionIntensity: 100000.0,
    iesIndex: 1,
    visible: true
});

每个光源均有独立的散射密度(光雾效果强度)、投影强度(表面光照强度)与 IES 索引。例如,路灯可配置为较强的体积散射效果配合相对较弱的表面投影,而车辆大灯则可反之。

着色器通过 QML中的 ExtendedSceneEnvironment 后处理效果遍历所有光源并累加其贡献。这套光源管理方案并不复杂,在数十个光源的场景下运行良好。

构建Demo场景

与真实光传输相比,单次散射在物理上并不严谨——但其误差对于目标视觉效果而言基本无关紧要。

体积光照系统完成后,笔者希望在具体场景中加以验证,因此构建了一个配有程序化生成道路系统的摩托车竞速 Demo。道路基于 Catmull-Rom 样条曲线从控制点生成平滑弯道,路灯则沿路径等间距放置。

程序化道路几何体在每个道路采样点计算切线与副法线向量,确保路灯垂直于道路方向正确定位。每盏路灯从已加载的 IES 配光文件中随机取用一个,并从调色板中指定颜色。

 

Untitled-Jan-29-2026-01-51-52-3176-PM

Demo 截图:赛道上数十道彩色光柱扑面而来,速度感十足


笔者还接入了 Qt 6.11 新增的运动向量缓冲区(Motion Vector Buffer),作为另一个 ExtendedSceneEnvironment 后处理效果实现真实的运动模糊效果:

void MAIN() {
    vec2 texcoord = TEXTURE_UV;
    if (FRAMEBUFFER_Y_UP < 0.0)
        texcoord.y = 1.0 - texcoord.y;

    vec4 motionvector = texture(MOTION_VECTOR_TEXTURE, texcoord);
    vec2 velocity = motionvector.xy;
    if (FRAMEBUFFER_Y_UP > 0.0)
        velocity.y = -velocity.y;

    const int SAMPLES = 16;
    const float BLUR_STRENGTH = 1.5;

    vec4 color = vec4(0.0);

    for (int i = 0; i < SAMPLES; i++)
    {
        float t = float(i) / float(SAMPLES - 1);
        vec2 offset = velocity * t * BLUR_STRENGTH;
        color += texture(INPUT, texcoord + offset);
    }

    color /= float(SAMPLES);

    FRAGCOLOR = color;
}

运动向量记录了每个像素在帧间的位移方向,着色器据此沿该方向进行模糊处理。与体积光照结合后,光柱随运动模糊拖曳而过,雾夜飞驰的速度感油然而生。

为提升可玩性,笔者加入了影响道路曲率的难度模式:普通模式弯道平缓,困难模式弯道收紧。此外还有圈速计时与积分系统。整个项目逐渐演变成一个完整的小游戏——这出乎原计划,但也因此成为光照系统极好的压力测试场景:30 余盏灯同时激活、交通灯持续变换、运动模糊全速运行,是检验性能是否达标的真实考场。完整源代码已对外开放,欢迎查阅。


Comments

Subscribe to our blog

Try Qt 6.11 Now!

Download the latest release here: www.qt.io/download

Qt 6.11 is now available, with new features and improvements for application developers and device creators.

We're Hiring

Check out all our open positions here and follow us on Instagram to see what it's like to be #QtPeople.