绘制无限大网格

很多很多的3D带编辑器的软件都会有网格来辅助物体的放置
比如说Unity

这东西在编辑放置物体的时候是个很强的参照,不得不有

那怎么做呢?

方案一:
自然是铺一个巨大巨大的正方形在地上啦,然后采样...,铺纹理就行啦
对于网格这种纹理,不需要真的去画一张,在Shader上用代码生成就行了
最简单的思路如下


float grid(vec2 uv,float gridSize,float lineWidth){
    vec2 g =abs(mod(uv,gridSize));
    float w = min(g.x,g.y);
    return step(w,lineWidth);
    //if(w<lineWidth)return 1;
    //return 0;
}

//in  main
void main(){
    colorOut = vec4(1,1,1,1.0 - grid(uv,5,0.2));
}

这样子简单是不假...但是呢,问题在于这里的线粗是固定的,这意味着在屏幕最远方的线是0.2粗,而最近的线也是0.2粗,那么..就会很奇怪,没有近大远小了。
简单的优化思路是利用glsl的求导函数对线宽做一个缩放

vec2 derivative = fwidth(uv);
lineWidth = lineWidth * max(derivative.x,derivative.y);

然而这个还是不能用...原因是生成的格子会有严重的锯齿...很哈人
如这样:

但是呢IQ大神指出: 对于格子状的程序生成纹理过滤是有解析解的,如这篇文章所讲述
于是就有了这个Shadowtoy

当然为了方便控制其中的参数,我对他做了一点点的改进

float gridTextureGradBox( in vec2 p,float scale,float lineWidth)
{
    const float N = 10.0; // grid ratio
	// filter kernel
    vec2 w = fwidth(p) + 0.01;
    w *= scale;
    p *= scale;
	// analytic (box) filtering
    vec2 a = p + 0.5*w;                        
    vec2 b = p - 0.5*w;           
    vec2 i = (floor(a)+min(fract(a)*N,lineWidth)-
              floor(b)-min(fract(b)*N,lineWidth))/(N*w);
    //pattern
    return (1.0-i.x)*(1.0-i.y);
}

进一步提出一个需求,在编辑器里很多都是大格子套小格子,实现如此只需要拿两个函数一重叠就好力

void main(){
    float bigGrid = gridTextureGradBox(fragPos3D.xz,0.5,0.05);
    float smallGrid = gridTextureGradBox(fragPos3D.xz,5,0.05);
    outColor = vec4(.8, .8, .8,1 - 0.5*smallGrid - 0.5*bigGrid );
}

这种方法有很大的问题哦,就是格子覆盖的范围会被你用的正方形大小限制~
而且呢,uv身为浮点数,一但过大,精度问题嘛嘿嘿嘿

这时候就有了方案二:

既然要做个无限大的格子
嗯...无限大,其实就是你无论怎么移动视角都能看到,那么也就是在屏幕空间内生成就行啦!!!!
基于此,在程序内只要传入一个覆盖了屏幕空间的四边形,如下:

  std::vector<fm::vector3> vecs{
    {1, 1, 0},{-1, -1, 0},
    {-1, 1, 0},{-1, -1, 0},
    {1, 1, 0},{1, -1, 0}
  }; 
  std::vector<unsigned int> indices{2,1,0,5,4,3};    

然后VBO,VAO,顶点属性,绑定啥的...不写了
那么在vertex shader中,只需要算出这个四边形对应在y=0平面上的部分就好啦
这实际上是个反投影过程

vec3 UnprojectPoint(dvec3 p) {
    vec4 unprojectedPoint = vec4(InvViewMat*InvProjMat * dvec4(p, 1.0)) ;
    return unprojectedPoint.xyz / unprojectedPoint.w;
}

但是由于我们缺失了深度值,还需要进一步的计算
把点分别反投影到近平面和远平面上,两投影点连线交于Y=0上,即为目标顶点。

vec3 eval_interscetion_xz(vec3 nearP,vec3 farP){
    double t = - nearP.y / (farP.y - nearP.y);
    return  vec3(nearP + t  * (farP - nearP));
}

这里会出现几种情况,在这我用正方形演示(因为我不会做棱柱而且正交相机也是可行的o((>ω< ))o
第一种是无相交点

那么就不会显示格子...GPU会帮我们处理错误的

第二种是有几点出了相机范围

那么GPU会帮我们裁剪的,也不用担心

第三种是所有点都在里面

那么就更没问题了!
所以该方案完全可行

那接下来就是需要把该计算好的点重新进行mvp变换,得到正确的深度,再把点送入gl_Position

而这几点,就是在y=0平面上,且大小刚好覆盖整个屏幕的四边形

拿到此处的深度很重要,你不会希望格子跑到其他物体上头去的

void main() {
    nearPoint = UnprojectPoint(dvec3(aPos.xy, -1.0)).xyz; // unprojecting on the near plane
    farPoint = UnprojectPoint(dvec3(aPos.xy, 1.0)).xyz; // unprojecting on the far plane
    gl_Position =vec4( ProjMat * ViewMat * vec4(vIntersection,1.0));
    gl_Position = gl_Position / gl_Position.w;
}

这里还有点点的小问题
第一呢就是理论上透视除法GPU会帮着做,但是我不除就会有BUG
第二个问题呢就是如果我把交点的世界坐标送入Fragment shader插值会出错,所以需要把近平面交点和远平面交点传过去再进行计算。。。。

这两个问题花了我一天,一天!!!>︿<

这个是bug拉满的版本的演示

很好的CULT视频,使我大脑旋转(

然后在Fragment Shader中在此计算出交点,利用交点来计算grid纹理

//Before main
in vec3 nearPoint;
in vec3 farPoint;
out vec4 outColor;

//in main  
float t = -nearPoint.y / (farPoint.y - nearPoint.y);
vec3 fragPos3D = nearPoint + t * (farPoint - nearPoint);
 float bigGrid = gridTextureGradBox(fragPos3D.xz,0.5,0.05);
float smallGrid = gridTextureGradBox(fragPos3D.xz,5,0.05);
outColor = vec4(.8, .8, .8,1 - 0.5*smallGrid - 0.5*bigGrid );

实际上还有点点的瑕疵,那就是太远处的格子叠在一块了很白很难看
用一个指数雾解决

float expFog(float dist){
  return exp2(-0.001 * dist * dist);
}

//in main
outColor = outColor * (expFog(distance(vec3(CamPos),fragPos3D)));

再叠加个x,y坐标有颜色,然后就得到了...

还不错欸,舒服

其他

实际上这次很大的灵感来源于 这篇文章 如何绘制一个无限大的网格
但我很不喜欢它里面的一些处理,包括把深度的计算延后到了FS中,这是很大的性能开销,简直扯淡,能放到vs里做的为什么要送到后面去?多了多少次次矩阵乘法没点数?
而且里面对于边缘消失的处理也不是很...好,还得算一个线性深度,开销++,给一个摄像机距离能咋样?
然后我找到了原文章How to make an infinite grid
原来如此...

那么代码就不再汇总了,Apex启动!

END