大数跨境

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

博客|基于Qt 6.11的轻量级实时体积光方案 Qt软件
2026-07-06
5
导读:基于真实IES配光文件构建体积光柱(Volumetric Light Shafts)傍晚薄雾中,路灯投射出独特







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

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


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


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



从单次散射(Single Scattering)出发


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


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



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


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



IES配光文件是什么?


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


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


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


解析后的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.00.01.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 effectfloat scatter = volumetricScatter(cameraPosition, worldPos, lightPosition, lightForward,                                  scatterDensity, iesIndex, omnidirectionality);vec3 volumetricLight = lightColor * lightIntensity * scatter;// Regular surface lighting with the same IES profilefloat 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 upfinalColor += (projectedLight + volumetricLight) * distanceAttenuation;

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


投影光照图案与体积光晕的叠加效果

多光源管理


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

tempLights.push({    position: Qt.vector3d(x, y, z),    forward: Qt.vector3d(dx, dy, dz),    omnidirectionality0.0,    color"lightblue",    intensity20,    scatterDensity0.4,    projectionIntensity100000.0,    iesIndex1,    visibletrue});

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


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



构建Demo场景


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


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


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


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余盏灯同时激活、交通灯持续变换、运动模糊全速运行,是检验性能是否达标的真实考场。完整源代码已对外开放,欢迎查阅。


END

往期推荐

博客 | Qt 6.11正式发布!

博客 | Qt新增的画布渲染功能

博客 | Qt Quick 3D 中的光照贴图烘焙及其他改进



【声明】内容源于网络
0
0
Qt软件
Qt Group是一家全球软件公司,致力于开发用户喜爱的应用程序和智能设备,深受行业领导者和全球超过 150 万开发人员的信赖。我们帮助客户在整个产品开发生命周期中提高生产力——从 UI 设计和软件开发到质量管理和部署。网址:qt.io
内容 567
粉丝 0
Qt软件 Qt Group是一家全球软件公司,致力于开发用户喜爱的应用程序和智能设备,深受行业领导者和全球超过 150 万开发人员的信赖。我们帮助客户在整个产品开发生命周期中提高生产力——从 UI 设计和软件开发到质量管理和部署。网址:qt.io
总阅读4.2k
粉丝0
内容567