CPU到GPU的法线映射

3

我正在创建一个地形网格,并按照这个SO回答的方法尝试将我的CPU计算的法线迁移到基于着色器的版本,以通过减少网格分辨率并在片段着色器中使用法线贴图来提高性能。

我正在使用MapBox高度图作为地形数据。瓦片看起来像这样:

enter image description here

每个像素的高程由以下公式给出:
const elevation = -10000.0 + ((red * 256.0 * 256.0 + green * 256.0 + blue) * 0.1);

我的原始代码首先创建了一个密集网格(256*256个由2个三角形组成的正方形),然后计算三角形和顶点法向量。为了获得视觉上令人满意的结果,我将高程除以5000来匹配场景中瓦片的宽度和高度(将来我会进行正确的计算以显示真实的高程)。
我使用了以下简单的着色器进行绘制:
顶点着色器:
uniform mat4 u_Model;
uniform mat4 u_View;
uniform mat4 u_Projection;

attribute vec3 a_Position;
attribute vec3 a_Normal;
attribute vec2 a_TextureCoordinates;

varying vec3 v_Position;
varying vec3 v_Normal;
varying mediump vec2 v_TextureCoordinates;

void main() {

  v_TextureCoordinates = a_TextureCoordinates;
  v_Position = vec3(u_View * u_Model * vec4(a_Position, 1.0));
  v_Normal = vec3(u_View * u_Model * vec4(a_Normal, 0.0));
  gl_Position = u_Projection * u_View * u_Model * vec4(a_Position, 1.0);
}

片段着色器:

precision mediump float;

varying vec3 v_Position;
varying vec3 v_Normal;
varying mediump vec2 v_TextureCoordinates;

uniform sampler2D texture;

void main() {

    vec3 lightVector = normalize(-v_Position);
    float diffuse = max(dot(v_Normal, lightVector), 0.1);

    highp vec4 textureColor = texture2D(texture, v_TextureCoordinates);
    gl_FragColor = vec4(textureColor.rgb * diffuse, textureColor.a);
}

它运行缓慢,但提供了视觉上令人满意的结果:

enter image description here

现在,我已经删除了所有基于CPU的法线计算代码,并用以下着色器替换了我的着色器:
顶点着色器:
#version 300 es

precision highp float;
precision highp int;

uniform mat4 u_Model;
uniform mat4 u_View;
uniform mat4 u_Projection;

in vec3 a_Position;
in vec2 a_TextureCoordinates;

out vec3 v_Position;
out vec2 v_TextureCoordinates;
out mat4 v_Model;
out mat4 v_View;

void main() {

  v_TextureCoordinates = a_TextureCoordinates;
  v_Model = u_Model;
  v_View = u_View;

  v_Position = vec3(u_View * u_Model * vec4(a_Position, 1.0));
  gl_Position = u_Projection * u_View * u_Model * vec4(a_Position, 1.0);
}

片段着色器:
#version 300 es

precision highp float;
precision highp int;

in vec3 v_Position;
in vec2 v_TextureCoordinates;

in mat4 v_Model;
in mat4 v_View;

uniform sampler2D u_dem;
uniform sampler2D u_texture;

out vec4 color;

const vec2 size = vec2(2.0,0.0);
const ivec3 offset = ivec3(-1,0,1);

float getAltitude(vec4 pixel) {

  float red = pixel.x;
  float green = pixel.y;
  float blue = pixel.z;

  return (-10000.0 + ((red * 256.0 * 256.0 + green * 256.0 + blue) * 0.1)) * 6.0; // Why * 6 and not / 5000 ??
}

void main() {

    float s01 = getAltitude(textureOffset(u_dem, v_TextureCoordinates, offset.xy));
    float s21 = getAltitude(textureOffset(u_dem, v_TextureCoordinates, offset.zy));
    float s10 = getAltitude(textureOffset(u_dem, v_TextureCoordinates, offset.yx));
    float s12 = getAltitude(textureOffset(u_dem, v_TextureCoordinates, offset.yz));

    vec3 va = (vec3(size.xy, s21 - s01));
    vec3 vb = (vec3(size.yx, s12 - s10));

    vec3 normal = normalize(cross(va, vb));
    vec3 transformedNormal = normalize(vec3(v_View * v_Model * vec4(normal, 0.0)));

    vec3 lightVector = normalize(-v_Position);
    float diffuse = max(dot(transformedNormal, lightVector), 0.1);

    highp vec4 textureColor = texture(u_texture, v_TextureCoordinates);
    color = vec4(textureColor.rgb * diffuse, textureColor.a);
}

现在它几乎瞬间加载,但是有些问题:

  • 在片段着色器中,我必须将高程乘以6而不是除以5000,才能得到接近原始代码的结果
  • 结果不理想。特别是当我倾斜场景时,阴影非常暗(倾斜得越多,阴影就越暗):

enter image description here

你能发现是什么原因导致了这种差异吗?

编辑:我创建了两个JSFiddles:

当你调整倾斜滑块时,问题就会出现。


这张256*256的图片包含每个像素的高程,而不是法线。我正在传递它来计算法线。很抱歉,我对OpenGL/WebGL的经验非常有限,所以我不知道你所说的高度偏移是什么意思,所以我的答案是否定的 :) - Tim Autin
1
哦,好的,那么是的,我正在做这个。但在新版本中,我的网格分辨率要低得多(32*32)。编辑:我将顶点位置除以5000,而不是乘以6。 - Tim Autin
我会尝试创建一个JSFiddle。是的,创建一个普通的地图服务器是另一个选项,但我需要为整个地球提供服务,因此仅针对高度图进行操作会很好,如果GPU上的法线图计算性能良好。您认为这种解决方案会出现性能问题吗? - Tim Autin
此外,这两个版本目前在以下网址上运行:http://176.150.209.228:8080/normals-cpu(基于CPU)和http://176.150.209.228:8080/normals(基于GPU)。 - Tim Autin
1
既然我看到这不是一个移动游戏,而且只在需要时才渲染,那么,我认为不会有性能问题(但你永远不知道客户端使用的硬件是什么)。现在我也理解了性能问题,因为法线贴图计算是在JavaScript中完成的,而不是在某个编译语言中完成的。 - lvella
显示剩余4条评论
1个回答

1

我找到了三个问题。

其中一个你通过试错发现并解决了,即你的高度计算比例错误。在CPU中,颜色坐标的变化范围是0到255,但在GLSL中,纹理值被规范化为0到1,因此正确的高度计算方式是:

return (-10000.0 + ((red * 256.0 * 256.0 + green * 256.0 + blue) * 0.1 * 256.0)) / Z_SCALE;

但是对于这个着色器的目的,-10000.00并不重要,所以你可以这样做:

return (red * 256.0 * 256.0 + green * 256.0 + blue) * 0.1 * 256.0 / Z_SCALE;

第二个问题是,您的x和y坐标的比例也是错误的。在CPU代码中,两个相邻点之间的距离为(SIZE * 2.0 / (RESOLUTION + 1)),但在GPU中,您将其设置为1。定义size变量的正确方式是:
const float SIZE = 2.0;
const float RESOLUTION = 255.0;

const vec2 size = vec2(2.0 * SIZE / (RESOLUTION + 1.0), 0.0);

注意,我将分辨率增加到 255,因为我假设这是您想要的(纹理分辨率的补数)。此外,这也需要与您定义的 offset 值相匹配。
const ivec3 offset = ivec3(-1,0,1);

为了使用不同的RESOLUTION值,您需要相应地调整offset,例如对于RESOLUTION == 127offset = ivec3(-2,0,2),即偏移量必须是<real texture resolution>/(RESOLUTION + 1),这限制了RESOLUTION的可能性,因为偏移量必须是整数。

第三个问题是在GPU中使用了不同的法线计算算法,这使我觉得比CPU上使用的方法具有较低的分辨率,因为您使用十字形的四个外部像素,但忽略了中心像素。看起来这并不是全部,但我无法解释它们如此不同。我尝试按照我认为的确切的CPU算法实现它,但它产生了不同的结果。相反,我不得不使用以下算法(类似但不完全相同),以获得几乎相同的结果(如果您将CPU分辨率增加到255):

    float s11 = getAltitude(texture(u_dem, v_TextureCoordinates));
    float s21 = getAltitude(textureOffset(u_dem, v_TextureCoordinates, offset.zy));
    float s10 = getAltitude(textureOffset(u_dem, v_TextureCoordinates, offset.yx));

    vec3 va = (vec3(size.xy, s21 - s11));
    vec3 vb = (vec3(size.yx, s10 - s11));

    vec3 normal = normalize(cross(va, vb));

这是原始的CPU解决方案,但是使用RESOLUTION=255: http://jsfiddle.net/k0fpxjd8/ 这是最终的GPU解决方案:http://jsfiddle.net/7vhpuqd8/

非常感谢!现在它们几乎完全相同了 :) 。为了使它们真正相同,我们可以在GPU示例中将网格分辨率增加到255。将其增加到63就足以获得不错的结果。我还将尝试使用GPU算法在CPU上计算法线贴图,这可能比我的每个顶点版本快得多。 - Tim Autin
如果你想的话,可以尝试使用WebAssembly,在浏览器上进行数值计算理论上会更快,但我从未使用过,也不了解初始化时间。 - lvella
谢谢,我会看一下! - Tim Autin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接