【Virtual Globe 渲染技术笔记】7 GPU 光线投射
GPU 光线投射(GPU Ray Casting)
GPU 专为光栅化三角形而设计,速度极快。
传统椭球渲染流程是:
- 用细分算法生成大量三角形
- GPU 并行光栅化
- 着色器计算颜色
这套流程虽然高效,但仍存在固有问题:
- 任何细分都不是完美的,各有优劣;
- 细分过粗 → 曲面失真;过细 → 性能/内存开销大,需要复杂的视点相关 LOD;
- 现代 GPU 算力增长远超内存带宽,频繁向总线发送大量三角形会拖慢性能。
光线追踪:另一种思路
光栅化从三角形到像素;
光线追踪从像素出发,逆着视线发射射线,求与物体的交点,再计算光照。
对椭球而言,射线与隐式曲面的交点有解析解,因此无需三角形网格即可渲染。
优势:
- 无限级细节:放大时不会出现“网格”或“锯齿”曲面;
- 无拓扑缺陷:没有极区细长三角形、IDL 跨越等问题;
- 内存极低:只存储椭球参数,无需巨型顶点缓冲。
如何在 GPU 上“光栅化”光线追踪?
现代 GPU 原生支持光栅化,但我们可以“借壳”:
-
构造椭球的轴对齐包围盒(12 个三角形)。
-
用光栅管线渲染这个盒子,前向面剔除,保证相机在盒内时仍能看见地球。
-
片元着色器里,针对每个像素发射射线:
- 起点:相机位置
ce_cameraEye
- 方向:从相机到片元世界坐标
normalize(worldPos - ce_cameraEye)
- 起点:相机位置
-
用解析公式判断射线与椭球是否相交。
- 需要椭球半径
(a,b,c)
; - 为提高效率,CPU 预先计算
1/radii²
作为 uniform 传入。
- 需要椭球半径
-
若相交,计算交点、法线、纹理坐标,用本章前面
LightIntensity()
等函数着色;
若不相交,discard
片元(下图 绿色/红色示例)。
完整片元着色器流程
- Listing 1 只判断相交,输出纯色。
// List-1 Base GLSL fragment shader for ray casting.
in vec3 worldPosition;
out vec3 fragmentColor;uniform vec3 ce_cameraEye;
uniform vec3 u_globeOneOverRadiiSquared;struct Intersection
{bool Intersects;float Time; // Time of intersection along ray
};Intersection RayIntersectEllipsoid(vec3 rayOrigin, vec3 rayDirection, vec3 oneOverEllipsoidRadiiSquared)
{// ...
}void main()
{vec3 rayDirection = normalize(worldPosition - ce_cameraEye);Intersection i = RayIntersectEllipsoid(ce_cameraEye, rayDirection, u_globeOneOverRadiiSquared);fragmentColor = vec3(i.Intersects ? 1.0 : 0.0, !i.Intersects ? 1.0 : 0.0, 0.0);
}
- Listing 2 计算交点位置、法线、纹理坐标,完成光照与纹理。
// List 2 Shading or discarding a fragment based on a ray cast.
vec3 GeodeticSurfaceNormal(vec3 positionOnEllipsoid, vec3 oneOverEllipsoidRadiiSquared)
{return normalize(positionOnEllipsoid * oneOverEllipsoidRadiiSquared);
}void main()
{vec3 rayDirection = normalize(worldPosition - ce_cameraEye);Intersection i = RayIntersectEllipsoid(ce_cameraEye, rayDirection, u_globeOneOverRadiiSquared);if (i.Intersects){vec3 position = ce_cameraEye + (i.Time * rayDirection);vec3 normal = GeodeticSurfaceNormal(position, u_globeOneOverRadiiSquared);vec3 toLight = normalize(ce_cameraLightPosition - position);vec3 toEye = normalize(ce_cameraEye - position);float intensity = LightIntensity(normal, toLight, toEye, ce_diffuseSpecularAmbientShininess);fragmentColor = intensity * texture(ce_texture0, ComputeTextureCoordinates(normal)).rgb;}else{discard;}
}
- Listing 3 把交点深度写入
gl_FragDepth
,解决深度缓冲区错误问题。
// List 3 Computing depth for a world-space position.
float ComputeWorldPositionDepth(vec3 position)
{vec4 v = ce_modelViewPerspectiveMatrix * vec4(position, 1.0);v.z /= v.w;v.z = (v.z + 1.0) * 0.5;return v.z;
}
性能与优化
- overdraw:包围盒外片元被丢弃,看似浪费,但现代 GPU 的动态分支与wavefront 一致性让开销很小。
- 更紧包围:用视口对齐凸多边形代替轴对齐盒,可进一步减少空射线,在顶点/片元负载间权衡。
- 无细分 → 无三角形传输瓶颈,显存占用极低。
局限与展望
- 椭球有封闭解,但通用场景需要复杂加速结构(BVH、KD-Tree),在动态场景下 GPU 实现困难;
- 软阴影、抗锯齿、多光源会导致射线数爆炸;
- GPU 光线追踪仍是研究热点。
尽管如此,GPU 光线投射椭球已能无缝嵌入光栅管线,成为细分网格的有力替代方案。
参考:
- Cozi, Patrick; Ring, Kevin. 3D Engine Design for Virtual Globes. CRC Press, 2011.