基于光线投射原理实现的体渲染
- 一、什么是体绘制?
- 二、为什么不直接用3D模型渲染
- 三、原理及部分代码解析
- 1、什么是光线?
- 2、什么是光线投射?
- 3、为什么需要光线投射3D纹理?
- 4、为什么必须是3D纹理?
- 5、为什么还需要1D纹理?
- 6、什么是光线投射?
- 7、为什么必须使用光线投射?
- 8、光线投射的完整流程
- 9、平衡细节锐度和整体柔和度
- 10、代码中的关键设计
- 11、性能优化策略
- 12、改进点
- 四、着色器代码
- 1、顶点着色器代码
- 2、片元着色器代码
一、什么是体绘制?
1、核心目标:将三维离散标量场(如医学CT、MRI数据)可视化为二维图像
2、技术难点:数据量大(百万级体素)、需表现内部结构(如半透明、遮挡关系)
3、常见应用:医疗影像、流体模拟、地质勘探
二、为什么不直接用3D模型渲染
数据本质差异:CT/MRI等数据是空间标量长,没有显式表面。
渲染结果:三角网格可视化结果,模型内部是空的,没内容;但是体渲染无论内部还是表面都是有内容的。
示例:肺部CT中,肺泡结构无法用三角网格描述。
三、原理及部分代码解析
1、什么是光线?
在体渲染中,光线是一个数学抽象概念,描述从相机出发穿过三维空间的虚拟视线。
2、什么是光线投射?
光线投射(Ray Casting)是一种体数据可视化算法,通过模拟光线在三维空间中的传播过程,将离散的体数据(如CT、MRI扫描的体素)渲染为二维图像。
其核心思想是:从每个像素反向投射一条光线穿过三维体数据,沿光线路径累积颜色和透明度,最终合成像素颜色。
3、为什么需要光线投射3D纹理?
3D纹理的作用:
存储原始体数据:每个体素(voxel)保存一个标量值,表示该空间位置的物理属性。
·医学CT:亨利单位
·流体模拟:密度、速度等
空间映射:通过(pos-minBound)/(maxBound-minBound)将模型坐标转换为纹理坐标
4、为什么必须是3D纹理?
三维数据本质:体数据本身是三维的(宽×高×深),无法用2D纹理表达。
硬件优化:GPU对3D纹理的插值(三线性插值)和缓存机制专门优化,采样效率远高于手动计算体素位置。
空间连续性:3D纹理的自动插值能平滑相邻体素间的密度值,避免“马赛克”伪影。
5、为什么还需要1D纹理?
从3D纹理中读取某个空间位置的标量值,但是最终要将其转换为颜色,着色器怎么知道这个标量值应该对应什么颜色呢?1D纹理就起到了这个作用。
语义映射:将标量值转换为可视属性
·输入:密度值(0~1)
·输出:RGBA颜色
示例:
# 肺部CT的典型传输函数
if density < 0.2: return (0,0,0,0) # 空气→全透明
elif 0.2<d<0.3: return (1,0.8,0.6,0.1) # 软组织→半透明肉色
else: return (1,1,1,0.8) # 骨骼→不透明白色
6、什么是光线投射?
光线投射(Ray Casting)是一种体数据可视化算法,通过模拟光线在三维空间中的传播过程,将离散的体数据(如CT、MRI扫描的体素)渲染为二维图像。
其核心思想是:从每个像素反向投射一条光线穿过三维体数据,沿光线路径累积颜色和透明度,最终合成像素颜色。
7、为什么必须使用光线投射?
无结构限制:不需要预先生成表面几何
全空间采样:能捕捉任意位置的细节
8、光线投射的完整流程
A[相机像素] --> B(发射光线)
B --> C{与体数据包围盒相交?}
C -->|否| D[丢弃/背景色]
C -->|是| E[沿光线步进采样]
E --> F[获取体素密度]
F --> G[传输函数映射颜色]
G --> H[累积颜色与透明度]
H --> I{透明度饱和?}
I -->|是| J[提前终止]
I -->|否| E
J --> K[输出最终颜色]
关键步骤
1)光线生成
原理:从相机每个像素发射一条视线方向的光线
vec3 rayDir = normalize(vDirection); // 视线方向
vec3 rayStart = vCameraModelPosition; // 相机位置
2)包围盒求交
目的:快速确定光线与体数据的有效交集区域
算法:
·计算光线与包围盒各面的交点tmin、tmax
·取最大tmin作为入口点,最小tmax作为出口点
vec2 hitBox(vec3 orig, vec3 dir) { /* 返回 near 和 far 值 */ }
3)光线步进(Ray Marching)
采样间隔:
△
t
=
3
s
t
e
p
s
(
保证覆盖最大体素对角线
)
△t=\frac{\sqrt3}{steps}(保证覆盖最大体素对角线)
△t=steps3(保证覆盖最大体素对角线)
for (float t = near; t < far; t += delta) {
vec3 pos = rayStart + t * rayDir; // 当前采样点
// 获取密度和颜色...
}
9、平衡细节锐度和整体柔和度
for (float t = bounds.x; t < bounds.y; t += delta) {
sampleStart = vCameraModelPosition + t * rayDir;
vec3 texCoord = (sampleStart - minBound) * voxelSizeInv;
float density = texture3D(baseTexture, texCoord).r;
vec4 color = texture1D(tfTexture, density);
color.rbg = pow(color.rbg, vec3(2.2));//从gamma空间转换到线性空间
color *= densityFactor * delta;
finalColor += T*color;
T*=1.0-color.a;
if (T<0.01) break;
}
densityFactor 是体渲染中平衡细节锐度与整体柔和度的关键参数:
锐化模式(大值):突出高密度结构,适合硬表面检测,光线被快速吸收(如浓雾),短距离内不透明。
柔和模式(小值):增强半透明效果,适合生物组织或流体可视化,光线缓慢衰减(如薄雾),长距离混合。
通过调整此参数,用户可以在不修改传输函数或数据的前提下,快速优化渲染结果的视觉风格。
与 steps 的协同:
当 densityFactor 较小时,需增加 steps 以保证采样足够深度,避免漏掉细节。
当 densityFactor 较大时,可减少 steps 以优化性能(因光线提前终止)。
10、代码中的关键设计
1)模型空间 → 纹理空间:
t
e
x
C
o
o
r
d
=
模型坐标
−
m
i
n
B
o
u
n
d
m
a
x
B
o
u
n
d
−
m
i
n
B
o
u
n
d
(
确保采样位置与体数据纹理对齐
)
texCoord=模型坐标-minBound\over maxBound-minBound(确保采样位置与体数据纹理对齐)
maxBound−minBound(确保采样位置与体数据纹理对齐)texCoord=模型坐标−minBound
2)伽马校正
// 采样后转线性空间
color.rgb = pow(color.rgb, vec3(2.2));
// 输出前转回sRGB空间
gl_FragColor.rgb = pow(finalColor.rgb, vec3(1.0/2.2));
11、性能优化策略
包围盒剪裁:减少无效采样次数
自适应步长:在低密度区域增大步长
提前终止:当透明度接近完全不透明时停止采样
12、改进点
1)增加参数以实现剖切(裁剪)功能;
2)性能优化:自适应步长
float currentStep = delta; // 基础步长
if (density < 0.01) { // 空区域加速
currentStep *= 4.0; // 增大步长
t += currentStep; // 直接跳过空区域
continue;
} else if (density < 0.1) {
currentStep *= 2.0; // 半空区域中等加速
}
// 正常采样...
3)使用八叉树优化体渲染性能,显著较少光线投射中的无效采样次数。
四、着色器代码
1、顶点着色器代码
#version 110
/* GLSL 1.10需要显式声明精度 (OpenGL ES要求) */
#ifdef GL_ES
precision highp float;
#endif
// 体数据采样步长
uniform float xStepSize,yStepSize,zStepSize;
// 体数据纹理和颜色纹理
uniform sampler3D baseTexture;
uniform sampler1D tfTexture;
// 体数据包围盒边界
uniform vec3 minBound,maxBound;
varying vec3 vDirection;//模型空间下的光线方向
varying vec3 vCameraModelPosition;//模型空间下的相机位置
void main(void)
{
// 计算裁剪空间的位置
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
vec4 cameraPos = (gl_ModelViewMatrixInverse * vec4(0, 0, 0, 1));
vCameraModelPosition = cameraPos.xyz/cameraPos.w;
vDirection = gl_Vertex.xyz/gl_Vertex.w- vCameraModelPosition;
}
2、片元着色器代码
#version 110
/* GLSL 1.10需要显式声明精度 (OpenGL ES要求) */
#ifdef GL_ES
precision highp float;
precision highp sampler3D;
precision highp sampler1D;
#endif
#define EPSILON 1e-3
// 体数据纹理和颜色纹理
uniform sampler3D baseTexture;
uniform sampler1D tfTexture;
uniform int steps;//采样步长,值越大,采样次数越多,效果越小,但性能越差
uniform float densityFactor;//值越大,颜色变化剧烈,细节边界变得锐利,可能出现不平滑的锯齿状过渡;值越小,颜色变化缓慢,叠加的颜色较多,导致整体视觉上更加柔和、模糊,增强了雾气感
// 体数据包围盒边界
uniform vec3 minBound,maxBound;
varying vec3 vDirection;//模型空间下的光线方向
varying vec3 vCameraModelPosition;//模型空间下的相机位置
const int maxSamples = 256;
vec2 hitBox(vec3 orig, vec3 dir) {
vec3 inv_dir = 1.0 / dir;// 光线方向倒数(处理正负方向)
vec3 tmin_tmp = (minBound - orig) * inv_dir;// 沿光线方向到达包围盒各轴最小边界的距离
vec3 tmax_tmp = (maxBound - orig) * inv_dir; // 沿光线方向到达包围盒各轴最大边界的距离
vec3 tmin = min(tmin_tmp, tmax_tmp);// 各轴向的最近交点
vec3 tmax = max(tmin_tmp, tmax_tmp);// 各轴向的最远交点
float near = max(max(tmin.x, max(tmin.y, tmin.z)),0.0);// 光线最终进入包围盒的距离
float far = min(tmax.x, min( tmax.y, tmax.z)); // 光线最终离开包围盒的距离
return vec2( near, far );
}
void main(void)
{
vec3 rayDir = normalize(vDirection);
vec2 bounds = hitBox( vCameraModelPosition, rayDir );
bounds.x -= 0.000001;
if ( bounds.x >= bounds.y) discard;//光线与包围盒无交点。
vec3 sampleStart = vCameraModelPosition + bounds.x * rayDir;
// 初始化颜色累积
vec4 finalColor = vec4(0.0);
const float opacityThreshold = 0.99; // 不透明度阈值
float T = 1.0;
// 光线步进采样
float delta = sqrt(3.0) / float(steps); // 均匀步长
vec3 voxelSizeInv = 1.0 / (maxBound - minBound);
for (float t = bounds.x; t < bounds.y; t += delta) {
sampleStart = vCameraModelPosition + t * rayDir;
vec3 texCoord = (sampleStart - minBound) * voxelSizeInv;
float density = texture3D(baseTexture, texCoord).r;
vec4 color = texture1D(tfTexture, density);
color.rbg = pow(color.rbg, vec3(2.2));//从gamma空间转换到线性空间
color *= densityFactor * delta;
finalColor += T*color;
T*=1.0-color.a;
if (T<0.01) break;
}
// 丢弃完全透明的片元
if (finalColor.a < EPSILON) discard;
finalColor.rgb = pow(finalColor.rgb, vec3(1.0/2.2));
gl_FragColor = finalColor;
}