如何使片段着色器中光线追踪球体与GL_POINTS对齐?
问题描述
我用GL_POINTS传入位置数据做了个简易着色器,能生成随深度变化的屏幕对齐方块片段。现在想在片段着色器里给每个方块画带背光阴影的简易光线追踪球体,参考ShaderToy实现了sphIntersect和sphNormal函数,但球体和方块片段不对齐,出现截断问题——核心是不知道怎么匹配球体和顶点位置的投影来对齐,求解决方法。
参考效果问题:
原代码如下:
顶点着色器
#version 460 layout(location = 0) in vec4 position; // position of each point in space layout(location = 1) in vec4 color; //color of each point in space layout(location = 2) uniform mat4 view_matrix; // projection * camera matrix layout(location = 6) uniform mat4 cam_matrix; //just the camera matrix out vec4 col; // color of vertex out vec4 posi; // position of vertex void main() { vec4 p = view_matrix * vec4(position.xyz, 1.0); gl_PointSize = clamp(1024.0 * position.w / p.z, 0.0, 4000.0); gl_Position = p; col = color; posi = cam_matrix * position; }
片段着色器
#version 460 in vec4 col; // color of vertex associated with this fragment in vec4 posi; // position of the vertex associated with this fragment relative to camera out vec4 f_color; layout (depth_less) out float gl_FragDepth; float sphIntersect( in vec3 ro, in vec3 rd, in vec4 sph ) { vec3 oc = ro - sph.xyz; float b = dot( oc, rd ); float c = dot( oc, oc ) - sph.w*sph.w; float h = b*b - c; if( h<0.0 ) return -1.0; return -b - sqrt( h ); } vec3 sphNormal( in vec3 pos, in vec4 sph ) { return normalize(pos-sph.xyz); } void main() { vec4 c = clamp(col, 0.0, 1.0); vec2 p = ((2.0*gl_FragCoord.xy)-vec2(1920.0, 1080.0)) / 2.0; vec3 ro = vec3(0.0, 0.0, -960.0 ); vec3 rd = normalize(vec3(p.x, p.y,960.0)); vec3 lig = normalize(vec3(0.6,0.3,0.1)); vec4 k = vec4(posi.x, posi.y, -posi.z, 2.0*posi.w); float t = sphIntersect(ro, rd, k); vec3 ps = ro + (t * rd); vec3 nor = sphNormal(ps, k); if(t < 0.0) c = vec4(1.0); else c.xyz *= clamp(dot(nor,lig), 0.0, 1.0); f_color = c; gl_FragDepth = t * 0.0001; }
解决思路与修正方案
问题核心是片段着色器的射线生成逻辑和坐标系不匹配,以及深度值计算错误,导致球体与点精灵的屏幕范围错位。以下是关键修正点:
- 统一坐标系:用相机空间射线
原代码硬编码相机位置,和实际相机空间不一致。相机空间下相机原点就是vec3(0),射线方向要从点精灵的局部坐标转换而来,而非全局屏幕坐标。 - 计算点精灵内的局部UV
用gl_PointCoord获取片段在点精灵内的相对位置,转换为[-1,1]范围,精准对应球体的局部空间。 - 匹配球体半径与点精灵大小
根据点精灵的屏幕尺寸和深度,动态计算球体半径,保证两者屏幕大小一致。 - 修复深度值计算
将光线追踪得到的相机空间深度,转换为OpenGL标准的NDC深度范围[0,1],避免深度错乱。
修改后的完整代码
顶点着色器
#version 460 layout(location = 0) in vec4 position; // position.w 为点大小系数 layout(location = 1) in vec4 color; layout(location = 2) uniform mat4 view_matrix; // 投影*视图矩阵 layout(location = 6) uniform mat4 cam_matrix; // 视图矩阵(转相机空间) layout(location = 7) uniform vec2 screen_size; // 新增:屏幕分辨率 out vec4 col; out vec4 cam_pos; // 相机空间下的点位置 out float point_size; // 点精灵的屏幕尺寸 void main() { vec4 clip_pos = view_matrix * vec4(position.xyz, 1.0); gl_PointSize = clamp(1024.0 * position.w / abs(clip_pos.z), 0.0, 4000.0); gl_Position = clip_pos; col = color; cam_pos = cam_matrix * vec4(position.xyz, 1.0); point_size = gl_PointSize; }
片段着色器
#version 460 in vec4 col; in vec4 cam_pos; // 相机空间下的球体中心 in float point_size; out vec4 f_color; layout (depth_less) out float gl_FragDepth; // 相机空间下射线-球体相交 float sphIntersect(in vec3 ro, in vec3 rd, in vec3 sph_center, in float sph_radius) { vec3 oc = ro - sph_center; float b = dot(oc, rd); float c = dot(oc, oc) - sph_radius * sph_radius; float h = b*b - c; if (h < 0.0) return -1.0; return -b - sqrt(h); // 返回近交点 } // 球体法向量计算 vec3 sphNormal(in vec3 pos, in vec3 sph_center) { return normalize(pos - sph_center); } void main() { vec4 base_color = clamp(col, 0.0, 1.0); // 1. 计算片段在点精灵内的局部UV(转为[-1,1]范围) vec2 uv = (gl_PointCoord - 0.5) * 2.0; // 2. 生成相机空间下的射线方向 vec2 half_point_size = point_size * 0.5; vec2 ndc_offset = uv * half_point_size / vec2(1920.0, 1080.0) * 2.0; vec3 rd = normalize(vec3(ndc_offset.x, ndc_offset.y, 1.0)); vec3 ro = vec3(0.0); // 相机空间下相机在原点 // 3. 设置匹配点精灵的球体参数 float sph_radius = point_size / 2.0 / 1920.0 * abs(cam_pos.z) * 2.0; vec3 sph_center = cam_pos.xyz; // 4. 射线相交检测与光照计算 float t = sphIntersect(ro, rd, sph_center, sph_radius); if (t < 0.0) { discard; // 丢弃球体外的片段,只保留球体 // 若要保留原方块,替换为:base_color = vec4(1.0); } else { vec3 light_dir = normalize(vec3(0.6, 0.3, 0.1)); vec3 hit_pos = ro + rd * t; vec3 normal = sphNormal(hit_pos, sph_center); float diff = clamp(dot(normal, light_dir), 0.0, 1.0); base_color.xyz *= diff; } // 5. 计算正确的NDC深度值 vec4 clip_hit_pos = view_matrix * vec4(ro + rd*t, 1.0); gl_FragDepth = (clip_hit_pos.z / clip_hit_pos.w + 1.0) * 0.5; f_color = base_color; }
关键修正说明
- 局部UV适配:
gl_PointCoord直接获取片段在点精灵内的相对位置,避免全局屏幕坐标导致的错位,确保射线只在点精灵范围内检测球体。 - 动态半径计算:根据点精灵的屏幕大小和深度,计算出相机空间下的球体半径,保证球体和点精灵的屏幕尺寸完全匹配。
- 深度值修正:将光线追踪得到的相机空间位置转换为裁剪空间后,计算标准NDC深度,确保球体深度和点精灵深度一致,解决遮挡错乱问题。
内容的提问来源于stack exchange,提问作者PlatinumFrog
相关产品推荐
相关产品推荐

