如何处理 openGL 中点球体的闪电(环境光、漫反射、镜面反射)

How to handle lightning (ambient, diffuse, specular) for point spheres in openGL

初始情况

我想在 openGL 中可视化模拟数据。 我的数据由粒子位置 (x, y, z) 组成,其中每个粒子都有一些属性(如密度、温度等),这些属性将用于着色。如果您想知道的话,那些 (SPH) 粒子(10 万到数百万)组合在一起,实际上代表行星。我想将这些粒子渲染为小型 3D 球体并添加环境光、漫反射光和镜面光。

现状与问题

  1. 以我为例:我在哪个坐标系中进行闪电计算? "best"通过管道传递各种组件的方式是什么?

我在视图space中看到很常见,也很直观。但是:不同片段位置的法线是在剪辑 space 坐标中的片段着色器中计算的(请参阅附加的片段着色器)。我真的可以将它们 "back" 转换为视图 space 以在视图 space 中对所有片段进行闪电计算吗?与剪辑 space 相比有什么优势吗?

  1. 如果我为每个球体使用网格,那么在视图中获取法线会更容易 space,但我认为使用几百万个粒子会大大降低性能,所以最好使用球体相交来实现,会你同意吗?

PS:我不需要模型矩阵,因为所有粒子都已经就位。

//VERTEX SHADER

#version 330 core
layout (location = 0) in vec3 position;
layout (location = 2) in float density;

uniform float radius;
uniform vec3 lightPos;
uniform vec3 viewPos;

out vec4 lightDir;
out vec4 viewDir;
out vec4 viewPosition;
out vec4 posClip;
out float vertexColor;


// transformation matrices
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    lightDir        = projection * view * vec4(lightPos - position, 1.0f);
    viewDir         = projection * view * vec4(viewPos - position, 1.0f);
    viewPosition    = projection * view * vec4(lightPos, 1.0f);
    posClip         = projection * view * vec4(position, 1.0f);

    gl_Position = posClip;
    gl_PointSize = radius;

    vertexColor = density;

}
  1. 我知道 gl_Position 变量会发生投影划分,这是否真的发生在从顶点传递到片段着色器的所有 vec4 上?如果不是,也许片段着色器中的计算是错误的?

片段着色器,其中法线和 diffuse/specular 剪辑 space 中的闪电计算:

//FRAGMENT SHADER

#version 330 core

in float vertexColor;
in vec4 lightDir;
in vec4 viewDir;
in vec4 posClip;
in vec4 viewPosition;

uniform vec3 lightColor;

vec4 colormap(float x); // returns vec4(r, g, b, a)

out vec4 vFragColor;


void main(void)
{
    // AMBIENT LIGHT
    float ambientStrength = 0.0;
    vec3 ambient = ambientStrength * lightColor;

    // Normal calculation done in clip space (first from texture (gl_PointCoord 0 to 1) coord to NDC( -1 to 1))
    vec3 normal;
    normal.xy = gl_PointCoord * 2.0 - vec2(1.0);    // transform from 0->1 point primitive coords to NDC -1->1
    float mag = dot(normal.xy, normal.xy);          // sqrt(x=1) = sqrt(x)
    if (mag > 1.0)                                  // discard fragments outside sphere
        discard;            
    normal.z = sqrt(1.0 - mag);                     // because x^2 + y^2 + z^2 = 1

    // DIFFUSE LIGHT
    float diff = max(0.0, dot(vec3(lightDir), normal));
    vec3 diffuse = diff * lightColor;

    // SPECULAR LIGHT
    float specularStrength = 0.1;
    vec3 viewDir = normalize(vec3(viewPosition) - vec3(posClip));
    vec3 reflectDir = reflect(-vec3(lightDir), normal);  
    float shininess = 64;
    float spec = pow(max(dot(vec3(viewDir), vec3(reflectDir)), 0.0), shininess);
    vec3 specular = specularStrength * spec * lightColor;  

     vFragColor = colormap(vertexColor / 8) * vec4(ambient + diffuse + specular, 1);

} 
  1. 现在这实际上 "kind of" 起作用了,但我感觉球体不面向光源的侧面也被照亮了,这不应该发生。我怎样才能解决这个问题?

一些奇怪的效果:此时光源实际上在左侧行星的后面(它只是在左上角达到峰值),仍然存在漫反射和镜面反射效果。这边其实应该挺黑的! =(

同时我在片段着色器中得到一些 glError: 1282 错误,我不知道它来自哪里,因为着色器程序实际编译和运行,有什么建议吗? :)

你画的东西实际上不是球体。他们只是从远处看起来像他们。如果您对此表示满意,那绝对没问题。如果您需要几何正确的球体(具有正确的尺寸和正确的投影),您需要进行适当的光线投射。 This 似乎是关于该主题的综合指南。

1。什么坐标系?

最后,由你决定。坐标系只需要满足一些要求。它必须保持角度(因为照明完全与角度有关)。如果你需要基于距离的衰减,它也应该是距离保持的。世界和视图坐标系通常满足这些要求。剪辑 space 不适合光照计算,因为角度和距离都没有保留。此外,gl_PointCoord 在通常的坐标系中 none。它有自己的坐标系,如果您知道它们之间的关系,您应该只将它与其他坐标系一起使用。

2。网格还是什么?

网格绝对不适合渲染球体。如上所述,光线投射或一些 screen-space 近似是更好的选择。这是我在我的项目中使用的示例着色器:

#version 330

out vec4 result;

in fData
{
    vec4 toPixel; //fragment coordinate in particle coordinates
    vec4 cam;     //camera position in particle coordinates
    vec4 color;   //sphere color
    float radius; //sphere radius
} frag;

uniform mat4 p; //projection matrix

void main(void)
{
    vec3 v = frag.toPixel.xyz - frag.cam.xyz;
    vec3 e = frag.cam.xyz;
    float ev = dot(e, v);
    float vv = dot(v, v);
    float ee = dot(e, e);
    float rr = frag.radius * frag.radius;

    float radicand = ev * ev - vv * (ee - rr);
    if(radicand < 0)
        discard;

    float rt = sqrt(radicand);


    float lambda = max(0, (-ev - rt) / vv); //first intersection on the ray
    float lambda2 = (-ev + rt) / vv;  //second intersection on the ray
    if(lambda2 < lambda) //if the first intersection is behind the camera
        discard;

    vec3 hit = lambda * v; //intersection point
    vec3 normal = (frag.cam.xyz + hit) / frag.radius;

    vec4 proj = p * vec4(hit, 1); //intersection point in clip space
    gl_FragDepth = ((gl_DepthRange.diff * proj.z / proj.w) + gl_DepthRange.near + gl_DepthRange.far) / 2.0;

    vec3 vNormalized = -normalize(v);
    float nDotL = dot(vNormalized, normal);
    vec3 c = frag.color.rgb * nDotL + vec3(0.5, 0.5, 0.5) * pow(nDotL, 120);    

    result = vec4(c, frag.color.a);
}

3。透视分割

透视分割不适用于您的属性。 GPU 对您通过 gl_Position 传递的数据进行透视除法,然后将它们转换为屏幕 space。但是除非你自己动手,否则你永远不会真正看到这种透视分割的位置。

4。黑暗中的光明

这可能是您在剪辑 space 中混合使用不同坐标系或进行光照计算的结果。顺便说一句,镜面反射部分通常不会乘以 material 颜色。这是直接在表面反射的光。它不会穿透表面(根据 material 会吸收一些颜色)。这就是为什么即使在黑色物体上,这些高光通常也是白色(或您拥有的任何浅色)。