0%

着色器(shader)

1.顶点着色器(Vertex shader)

用于描述顶点特性(如颜色、位置等)。顶点指的是二维或者三维空间中的的一个点。

内置变量:

vec4 gl_Position 表示顶点位置

float gl_PointSize 表示点的尺寸(像素数)

2.片元着色器(Fragment shader)

进行逐片元的处理过程如光照。片元,可以将其理解为像素。

内置变量:

vec4 gl_FragColor 指定片元颜色(RGBA格式)

绘制过程

3.使用着色器的WebGL程序

将位置信息从JavaScript程序中传给顶点着色器。可以通过attribute变量uniform变量

attribute变量: 传输与顶点相关数据。给顶点着色器使用。只能是vec2,vec3,vec4,float,mat2,mat3,mat4类型。最少支持8个。必须全局。

uniform变量: 对于顶点相同的数据(与顶点无关)。给片元着色器使用。顶点着色器中最少支持128个,片元着色器中最少支持16个。必须全局。

声明(例):

1
2
attribute vec4 a_Position;
uniform vec4 u_FragColor;

获取attribute,uniform变量:

1
2
3
4
let a_Position = gl.getAttribLocation(gl.program,"a_Position");
//返回attribute变量位置,否则-1(具有webgl_或者gl_前缀或变量不存在)
let u_FragColor = gl.getUniformLocation(gl.program,"u_FragColor");
//返回uniform变量位置,否则null

attribute,uniform变量赋值:

其中第2,3分量默认为0.0,第四分量默认1.0。

WebGLRenderingContext.vertexAttrib[1234]fv

WebGLRenderingContext.uniform[1234][fi]v

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
gl.vertexAttrib1f(a_Position,v1);
gl.vertexAttrib2f(a_Position,v1,v2);
gl.vertexAttrib3f(a_Position,v1,v2,v3);
gl.vertexAttrib4f(a_Position,v1,v2,v3,v4);
//或者用以v结尾函数版本
let position = new Float32Atrray([1.0, 2.0, 3.0, 1.0]);
gl.vertexAttrib4fv(a_Position,position)

gl.uniform1f(u_FragColor,v1);
gl.uniform2f(u_FragColor,v1,v2);
gl.uniform3f(u_FragColor,v1,v2,v3);
gl.uniform4f(u_FragColor,v1,v2,v3,v4);
//或者用以v结尾函数版本
let color = new Float32Atrray([1.0, 2.0, 3.0, 1.0]);
gl.uniform4fv(u_FragColor,color)

WebGL函数命名规范:<基础函数名><参数个数><参数类型>

着色器初始化:

着色器程序由OpenGL ES着色器语言(GLSL ES) 编写。在JavaScript以字符串形式编写,传给WebGL系统。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
let VSHADER_SOURCE =
"attribute vec4 a_Position;\n" +
"void main() {\n" +
" gl_Position = a_Position;\n" +
" gl_PointSize = 10.0;\n" +
"}\n",
let FSHADER_SOURCE =
"precision mediump float;\n" +
"uniform vec4 u_FragColor;\n" +
"void main() {\n" +
" gl_FragColor = u_FragColor;\n" +
"}\n",

// Create shader object
let vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, VSHADER_SOURCE);
gl.compileShader(vertexShader);
let fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, FSHADER_SOURCE);
gl.compileShader(fragmentShader);

// Create a program object
let program = gl.createProgram();

// Attach the shader objects
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);

// Link the program object
gl.linkProgram(program);
gl.useProgram(program);

缓冲区对象

1
2
gl.vertexAttrib4fv(a_Position,position)
gl.uniform4fv(u_FragColor,color)

我们使用上面的方法每次只能传入并绘制一个点,而对于多个顶点组成的对象需要一次性传入多个顶点到着色器中。
缓冲区对象可以一次性传入多个顶点数据。其实WebGL系统中的一块内存区域,可以一次性向缓冲区对象中填充大量数据,供着色器使用。

使用缓冲区对象

传入顶点数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//JS中的Array未对同类型大量元素数组做优化,所以使用类型化数组Float32Array
let vertices = new Float32Array([0.0, 0.5, -0.5, -0.5, 0.5, -0.5])
let vertexBuffer = gl.createBuffer();
// Bind the buffer object to target
// target参数表示缓冲区对象的用途
// 参数gl.ARRAY_BUFFER表示缓冲区中包含了顶点数据。
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
// Write date into the buffer object
// 不能直接向缓冲区对象写入数据,只能向target上的缓冲区对象写入数据,所以必须先进行绑定。
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
// Assign the buffer object to a_Position variable
//gl.vertexAttribPointer(location, size, type, normalize, stride, offset)
//size指缓冲区中每个顶点的分量长度。如为2则默认每个顶点第三分量为0,第四分量为1。
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
// Enable the assignment to a_Position variable
gl.enableVertexAttribArray(a_Position);
//关闭分配了缓冲区的attribute变量
// gl.disableVertexAttribArray(a_Position);

绘制

1
2
3
4
5
//gl.drawArrays(mode, first, count)
//mode指基本图形类型如POINTS、TRIANGLES、LINE_LOOP等
//first为0时,指从缓冲区中第1个坐标开始画起
//count代表画多少次,实际上着色器执行了count次
gl.drawArrays(gl.POINTS, 0, 3);

  1. 找到性能瓶颈,尝试降低CPU或者GPU的时钟频率去测试哪个效率低

  2. 纹理受限,可以采取 减少canvas的长宽或者使用低分辨率的纹理测试;webgl 纹理绑定伸展和收缩效果时,gl.NEAREST 是最快的但会产生块状效果,gl.LINER因为是取平均值,会产生模糊

  3. 将Mip映射应用于纹理贴图

  4. 处理webgl丢失上下文的问题

  5. 不要经常切换program,在切换program和在着色器中使用if else语句都需要进行考量

  6. 避免在顶点数组数据中使用常量

  7. 在webgl中,使用drawElements()的gl.TRIANGLE_STRIP 结合退化三角形 比使用drawArrays()的gl.TRIANGLE方式节省内存,并且减少使用drawArrays和drawElements的次数

  8. 顶点组织顺序按照数组排序,不要使用乱序,因为难以命中缓存

  9. 减少使用drawArrays和drawElements的次数

  10. 避免绘制时从GPU读回数据或状态,例如,gl.getError() gl.readPixels(), 影响流水线的实现

  11. 用webgl inspector找出冗余的调用,因为webgl是的状态是跨帧持续的,减少使用改变webgl状态的方法。比如gl.enable(XXX),只执行一次就行了

  12. 用细节层次简化模型(LOD技术)

  13. 避免在shader中做逻辑判断,比如if else。

    有的人可能会很疑惑,为何要这样?这和GPU的基本调度有关系,GPU的基本调度单位叫做wavefront, 就是指一组完全相同的计算指令,在GPU的几个计算单元中并行执行,每一个指令的输入数据不同而已。这样并行度很高,可以极大程度提高性能。但是一旦引入if else,就会把wavefront破坏掉,比如现在有10个计算单元在并发执行,但是碰到if,在5个计算单元中为true, 在5个计算单元中为false, 这样会造成新的计算指令,那么之前的并行运算将无法继续。新的计算指令需要排队等待执行,或者新的指令要转移到新的计算单元上,这个过程涉及到数据的复制转移,会比较耗时,会严重破坏并行度。

    但是有些场景下,shader中完全不用逻辑判断又不行,那该如何呢?可以考虑使用shader的内置函数,比如step函数,案例如下:

    float a;

    if(b >1){

    a = 1;

    }else{

    a = 0.5;

    }

    可以优化为:

    float a;

    float temp = step(b, 1);

    a = temp * 0.5 + (1 - temp);

  14. 减少三角形数量

    较少三角形的数量大体上可以从以下几个角度入手:

    (1). 空间分割技术:包括八叉树,四叉树做空间分割,将不在当前可视区域物体剔除掉

    (2). 遮挡检测技术:视锥体范围内,有些物体会被前面的物体遮挡,这些被遮挡的物体其实是不需要渲染的。遮挡查询有多种技术方案实现,比如通过扩扑性,硬件遮挡查询。扩扑性比较麻烦,我这里推荐硬件遮挡查询技术,实现起来相对比较容易。

    (3). LOD技术:根据物体距离摄像头的距离,动态调节物体三角形的数量。

    (4). 图元类型的优化:使用GL_TRIANGLE_FAN或者GL_TRIANGLE_STRIP替代GL_TRIANGLES,因为这样可以重用顶点,减少三角形的数量。

    (5).使用顶点索引的方法做渲染:使用glDrawElements替代glDrawArrays,因为前者通过索引的方式可以减少三角形的数量。

  15. 纹理的优化

    (1). 纹理的长宽最好是2的幂。

    (2). 纹理压缩:纹理压缩在opengl es 3.0和webgl 2.0上有比较好的支持,经压缩后的纹理可以减少图形数据,节省宽带。常见的压缩格式为ETC,Khronos公司提供有ETC格式压缩的免费压缩包,在opengl/webgl程序中使用glCompressedTexImage2D函数加载被压缩的纹理。

    (3). 纹理的上传:传统的纹理上传比较耗时,可以考虑使用两个PBO上传纹理,性能会有较大的提升。

    (4). 纹理的合成:如果有很多个小的纹理,每一个纹理单独加载,效率比较低下。可以考虑将多个小纹理合成到一个纹理上,仅仅加载一次,然后在程序中使用的时候,使用不同的纹理坐标范围来加载不同的纹理。

  16. 减少系统内存向GPU内存传送数据的次数

    (1). 尽可能使用VBO/VAO

    (2). 在opengl es 3.0/webgl 2.0上可以使用Transform Feedback, 该方案可以使用GPU做通用运算,把计算的结果存入VBO中,在后期的渲染流程中使用该VBO作为输入。

    (3). 批次合并:比如一个最小包围体内有多个物体,可以将这些物体的三角形合并在一起,一次性的发送到GPU。

    (4). 使用instance: 如果要渲染多个重复的物体,可以使用instance特性。

WebGL优化点

物体旋转

使用模型视图投影矩阵变化顶点坐标。

根据鼠标位移差值,计算旋转矩阵。

1
2
3
4
5
viewProjMatrix.setPerspective()
viewProjMatrix.lookAt()
------
g_MvpMatrix.set(viewProjMatrix)
g_MvpMatrix.roate()

选中物体

使用颜色缓冲区方法(简单)
  1. 当点击时,整个对象重绘成单一红色。

  2. 读取鼠标点击处的颜色。

  3. 使用立方体原来的颜色重绘。

  4. 对比第二步读取到的颜色。

1
2
gl.readPixels(x, y, width, height, format, type, pixels)
//读取颜色缓冲区中x,y,width,height参数确定的像素值,保存在pixels中

例如对于立方体的多个表面,将表面编号写入组成每个表面各个顶点,将编号数组传入顶点着色器。当点击时,首先在顶点着色器中将每个面根据编号计算每个面的颜色α值,并在颜色缓冲区中重绘。即可通过readPixels获取颜色值,得到点击的表面编号。之后根据表面编号,再传入顶点着色器对相应面进行所需操作。

HUD(平视显示器)

再三维场景上叠加文本或者二维图形信息。

使用两个canvas叠加。背景透明。

雾化

线性雾化:雾化程度取决于与视点之间的距离。某一点的雾化程度可以定义成雾化因子。

片元颜色 = 物体表面颜色 x 雾化因子 + 雾的颜色 x (1 - 雾化因子)

w分量

gl_Position的w分量为顶点的视图坐标的z分量乘以-1。在视图坐标系中,视点在原点,视线沿着Z轴负方向,观察者看到的物体其视图坐标系值z分量都为负数,所以其w值可以直接近似为顶点与视点的距离。

绘制圆形点

将方形的点剔除不需要的片元。

片元着色器中内置变量

1
2
vec4 gl_FragCoord  //片元窗口坐标
vec4 gl_PointCoord //片元在被绘制的点内的坐标(0.0到1.0)

在片元着色器中,将距离点的中心(0.5,0.5)超过0.5的片元剔除。

1
2
distance(gl_PointCoord, vec2(0.5, 0.5)) //计算距离函数
discard //放弃当前片元语句

α混合

实现半透明效果,需要用到颜色的α分量。需要开启WebGL的α混合功能。

1
2
3
4
gl.enable(gl.BLEND) //开启混合功能
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHAD)
//指定混合函数 参数指定进行混合操作的函数。
//加法混合---gl.blendFunc(gl.SRC_ALPHA, gl.ONE)
混合函数

混合后的颜色 = 源颜色 * src_factor + 目标颜色 * dst_factor

源颜色: 待混合进去的颜色。上层颜色

目标颜色: 待被混合的颜色。下层颜色

透明与不透明共存

透明后如果开启了隐藏面消除功能,则被隐藏的片元则不会绘制。所以不会发生混合过程。但是关闭隐藏面消除功能会使物体前后关系乱套。所以要实现透明与不透明共存:

  1. 开启隐藏面消除功能 gl.enable(gl.DEPTH_TEST)
  2. 绘制所有不透明物体
  3. 锁定用于隐藏面消除的深度缓冲区的写入操作,使其只读深度缓冲区: 是一个中间对象,帮助进行隐藏面消除,存储深度信息:每个像素的归一化坐标z值。其对比z值,舍弃隐藏的片元,不会写入颜色缓冲区。gl.depthMask(false)
  4. 绘制所有半透明物体,需要按照深度排序,从后往前绘制。
  5. 释放深度缓冲区,可读可写。gl.depthMask(true)

切换着色器

当对于不同物体需要不同着色器进行绘制时,需要切换使用。

为多个着色器对象创建多个程序对象,使用gl.useProgram(program)进行切换。

渲染到纹理

加载三维模型

OBJ文件格式
  1. (#)开头行为注释

  2. 材质文件存储在外部MTL格式文件。mtllib<外部材质文件名>

  3. 模型名称 <模型名称>

  4. 顶点坐标 v x y z [w]

  5. 指定某个材质 。列举使用这个材质的表面。材质被定义在引用的MTL文件中。

    1
    2
    3
    4
    usemtl<材质名>
    f v1 v2 v3 v4 ···

    f v1//vn1 v2//vn2 v3//vn3 ···

    其中v1为顶点索引值,从1开始。

    vn1,vn2为法线向量索引,从1开始。

MTL文件格式
  1. 定义一个新材质newmtl<材质名>

  2. 使用Ka、Kd和Ks定义表面环境色、漫反射、高光色。使用RGB格式,区间为[0.0,1.0]

    1
    2
    3
    Ka 0.000000 0.000000 0.000000
    Kd 0.000000 0.000000 0.000000
    Ks 0.000000 0.000000 0.000000
  3. Ns指定高光色权重,Ni指定表面光学密度,d指定透明度,illum指定光照模型。

响应上下文丢失

当其他程序接管了图形硬件,或者操作系统休眠,浏览器会失去这些资源,webgl绘图上下文就会丢失。

1
2
webglcontextlost事件 当WebGL上下文丢失时触发
webglcontextrestored事件 当浏览器完成WebGL系统重置后触发

当丢失上下文时,getWebGLContext()函数获得渲染上下文对象gl就失效了。在浏览器重置WebGL后需要重新完成在之前gl对象上的所有操作。如创建缓冲区,纹理对象,初始化着色器等。

一、相关概念

大地坐标系(B, L, H) (纬度、经度、高度):B为过坐标点椭球面的法线与赤道面交角,L为过坐标点的子午线与起始子午线的夹角,H为坐标点沿法线到椭球面的距离。

空间直角坐标(X, Y, Z):Z轴与旋转椭球的短轴重合,向北为正,X轴与赤道面和首子午面的交线重合,向东为正。Y轴与XZ平面垂直构成右手系。

参心坐标系:

参考椭球的几何中心为原点的大地坐标系。通常分为:参心空间直角坐标系(以x,y,z为其坐标元素)和参心大地坐标系(以B,L,H为其坐标元素)。

地心坐标系:

以地球质心(总椭球的几何中心)为原点的大地坐标系。通常分为地心空间直角坐标系(以x,y,z为其坐标元素)和地心大地坐标系(以B,L,H为其坐标元素)。

椭球长半轴:

椭球扁率:

椭球短半轴:

椭球第一偏心率 :

椭球第二偏心率 :

卯酉圈曲率半径N:子午圈曲率半径M:

补充:

二、转换方法

1.已知WGS–84大地坐标(BLH)(单位:度.分秒)

1)BLH》》十进制度》》弧度制

​ 可参考EXCEL中方法:

​ RADIANS(25*B/9-2*INT(B)/3-INT(100*B)/90)

2)转为北京54空间直角坐标系(克拉索夫斯基椭球)(BLH->XYZ)

​ 参考公式:

BLH2XYX

​ PS:这里WGS84为地心坐标系而北京54为参心坐标系,(个人认为因为他们空间直角坐标系的轴方向一致,所以此处只需要添加XYZ的坐标偏移参数无需其他是三个旋转参数以及一个尺度参数),计算完后需要给XYZ加上常量,具体数值如下:

​ X:-22,Y:+188,Z+30.5

3)北京54的空间直角坐标系转换为其大地坐标系( XYZ → BLH )

​ 参考方法:

XYZ2BLH

​ 求解方法:

​ 1.迭代法:

​ 取B初值为:

​ C#代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//输入XYZ
double X = -2368953, Y = 5382025, Z = 2462584;
//相关参数
double a, N, W, e1, r, sinB;
double B1, B0, H1, H0, B, L, H;
a = 6378245;
e1 = 0.006693421622966;
r = X * X + Y * Y;
L = Math.Atan(Y / X) + Math.PI;
B0 = Math.Atan(Z / Math.Sqrt(r));
sinB = Math.Sin(B0);
W = Math.Sqrt(1 - e1 * sinB * sinB);
N = a / W;
H0 = Z / sinB - N * (1 - e1);
int maxIter = 100;
int iter = 0;
while (true)
{
iter++;
B1 = Math.Atan2(Z * (N + H0), Math.Sqrt(r) * (N * (1 - e1) + H0));
sinB = Math.Sin(B1);
W = Math.Sqrt(1 - e1 * sinB * sinB);
N = a / W;
H1 = Z / sinB - N * (1 - e1);
if ((Math.Abs(B1 - B0) < Math.Pow(10, -15) && Math.Abs(H1 - H0) < Math.Pow(10, -15)) || iter > maxIter)
{
break;
}
B0 = B1;
H0 = H1;
}
B = B1;
H = H1;

​ 2.直接求解

​ 参考公式:

XYZ2BLH2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//输入XYZ
double X = -2368953, Y = 5382025, Z = 2462584;
double a = 6378245, b = 6356863.0188;
double N, W, sb;
double e2 = 0.00673852540614652;
double e1 = 0.00669342161454287;
double r = Math.Sqrt(X * X + Y * Y);
double alpha = Math.Atan(Z * a / (r * b));
double cosal = Math.Cos(alpha);
double sinal = Math.Sin(alpha);
double L = Math.Atan(Y / X) + Math.PI;
double B = Math.Atan((Z + e2 * b * sinal * sinal * sinal) / (r - e1 * a * cosal * cosal * cosal));
sb = Math.Sin(B);
W = Math.Sqrt(1 - e1 * sb * sb);
N = a / W;
double H = r / Math.Cos(B) - N;
//结果为BLH

​ 3.EXCEL中:

​ 暂过~

4)结果BLH为弧度》》转为度分秒

​ 参考EXCEL中公式:

​ 9*B*180/PI()/25+2*INT(B*180/PI())/5+INT(60*B*180/PI())/250

​ PS:后面的计算,BLH依然用弧度制,这里只是提供一个方法

5)使用高斯投影中央经线为114度

​ 参考公式:

高斯正算x

高斯正算y

高斯正算参数2

​ C#代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//输入椭球参数及坐标点
double a = 6378245, f = 0.00335232986925913;
//L0为中央子午线弧度制 这里为114度
double B = 0.398992580521256, L = 1.98543736115197, L0 = 1.9896753472735356;
double x, y;

double b, c, e1, e2; //短半轴,极点处的子午线曲率半径,第一偏心率,第二偏心率
double l, W, N, M, daihao;//W为常用辅助函数,M为子午圈曲率半径,N为卯酉圈曲率半径
double X;//子午线弧长,高斯投影的坐标
double ruo, ita, sb, cb, t;
double[] m = new double[5];
double[] n = new double[5];
b = a * (1 - f);
e1 = Math.Sqrt(a * a - b * b) / a;
e2 = Math.Sqrt(a * a - b * b) / b;
c = a * a / b;
m[0] = a * (1 - e1 * e1);
m[1] = 3 * (e1 * e1 * m[0]) / 2.0;
m[2] = 5 * (e1 * e1 * m[1]) / 4.0;
m[3] = 7 * (e1 * e1 * m[2]) / 6.0;
m[4] = 9 * (e1 * e1 * m[3]) / 8.0;
n[0] = m[0] + m[1] / 2 + 3 * m[2] / 8 + 5 * m[3] / 16 + 35 * m[4] / 128;
n[1] = m[1] / 2 + m[2] / 2 + 15 * m[3] / 32 + 7 * m[4] / 16;
n[2] = m[2] / 8 + 3 * m[3] / 16 + 7 * m[4] / 32;
n[3] = m[3] / 32 + m[4] / 16;
n[4] = m[4] / 128;
X = n[0] * B - n[1] / 2 * Math.Sin(B * 2) + n[2] / 4 * Math.Sin(B * 4) - n[3] / 6 * Math.Sin(B * 6) + n[4] / 8 * Math.Sin(B * 8);
//X = n[0] * B - Math.Sin(B) * Math.Cos(B) * ((n[1] - n[2] + n[3]) + (2 * n[2] - (16 * n[3] / 3.0)) * Math.Sin(B) * Math.Sin(B) + 16 * n[3] * Math.Pow(Math.Sin(B), 4) / 3.0);
l = L - L0;//弧度 ruo无用
ita = e2 * Math.Cos(B);
sb = Math.Sin(B);
cb = Math.Cos(B);
W = Math.Sqrt(1 - e1 * e1 * sb * sb);
N = a / W;
t = Math.Tan(B);
ruo = (180 / Math.PI) * 3600;
x = (X + N * sb * cb * l * l / 2 + N * sb * cb * cb * cb * (5 - t * t + 9 * ita * ita + 4 * ita * ita * ita * ita) * l * l * l * l / 24 + N * sb * cb * cb * cb * cb * cb * (61 - 58 * t * t + t * t * t * t) * l * l * l * l * l * l / 720);
y = (N * cb * l + N * cb * cb * cb * (1 - t * t + ita * ita) * l * l * l / 6 + N * cb * cb * cb * cb * cb * (5 - 18 * t * t + t * t * t * t + 14 * ita * ita - 58 * ita * ita * t * t) * l * l * l * l * l / 120);
y = y + 500000;

​ 6)转为独立坐标系(平面四参数转换模型)

XY2独立

​ X0与Y0为坐标平移量,cos α,sin α 为坐标旋转因子,m为缩放因子。x1、y1为上一步的到的高斯投影面下的坐标;

​ 需要已有的地方坐标和高斯平面坐标采用坐标相似变换求其转换参数。

​ 具体看另一个仓库中的脚本工具,已经基于arcpy脚本实现 最新的深圳独立坐标与84坐标的互转。

WebGL坐标系统和纹理坐标

1.WebGL坐标系统:

先简单的认为是右手坐标系。

  1. x轴最左边为-1,最右边为1;
  2. y轴最下边为-1,最上边为1;
  3. z轴朝向你的方向最大值为1,远离你的方向最大值为-1;
2.纹理坐标

WebGL使用s和t命名纹理坐标(st坐标系统)。(还有以uv命名)

纹理图像四个角坐标为:左下(0.0,0.0),左上(0.0,1.0),右上(1.0,1.0),右下(1.0,0.0)。纹理坐标与图像自身尺寸无关,其右上角坐标始终是(1.0,1.0)。

讲纹理坐标与顶点坐标相对应,来确定怎样将纹理图像显示到相应的几何顶点坐标之间。

3.取样器变量

基本取样器类型: sampler2DsampleCube 只能是uniform变量。

数量受着色器支持的纹理单元最大数量限制。

纹理使用

1.着色器实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'attribute vec2 a_TexCoord;\n' +
'varying vec2 v_TexCoord;\n' +
'void main() {\n' +
' gl_Position = a_Position;\n' +
' v_TexCoord = a_TexCoord;\n' +
'}\n';

let FSHADER_SOURCE =
'precision mediump float;\n' +
'uniform sampler2D u_Sampler;\n' +
'varying vec2 v_TexCoord;\n' +
'void main() {\n' +
' gl_FragColor = texture2D(u_Sampler, v_TexCoord);\n' +
'}\n';
let a_Position = gl.getAttribLocation(gl.program, "a_Position");
let a_PointSize = gl.getAttribLocation(gl.program, "a_PointSize");
let a_TexCoord = gl.getAttribLocation(gl.program, "a_TexCoord");
//获取取样器变量u_Sampler
let u_Sampler = gl.getUniformLocation(gl.program, "u_Sampler");

顶点着色器中接受纹理坐标a_TexCoord,光栅化后传递给片元着色器。

片元着色器中获取纹理像素(纹素)颜色。使用GLSL ES中:

texture2D(sampler2D sampler, vec2 coord)```
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

从sampler指定的纹理上获取coord指定的纹理坐标处的像素。

**参数:**

sampler

> 纹理单元编号(texture unit number)。专用于纹理的数据类型```sampler2D```:绑定到```gl.TEXTURE_2D```的纹理数据类型

coord

> 纹理坐标。

**返回值:**

> 纹理坐标处的像素颜色值。格式由```gl.texImage2D()```的internalformat参数决定。

纹理放大缩小方法的参数决定WebGL系统将以何种方式内插处片元。将```texture2D()```函数返回值赋值给```gl_FragColor```,即片元着色器将当前片元染成这个颜色。最终画出纹理。

#### 2.设置纹理坐标

向顶点着色器传入顶点坐标,在光栅化后传递给片元着色器。
```javascript
//顶点坐标、顶点尺寸、纹理坐标
let verticesSizeColorTexCoords = new Float32Array([
-0.5, 0.5, 10.0, 0.0, 1.0,
-0.5, -0.5, 20.0, 0.0, 0.0,
0.5, 0.5, 30.0, 1.0, 1.0,
0.5, -0.5, 40.0, 1.0, 0.0
])
let vertexSizeColorTexCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER,vertexSizeColorTexCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, verticesSizeColorTexCoords, gl.STATIC_DRAW);
let FSIZE = this.verticesSizeColorTexCoords.BYTES_PER_ELEMENT;
gl.vertexAttribPointer(this.a_Position, 2, gl.FLOAT, false, FSIZE * 5, 0);
gl.enableVertexAttribArray(this.a_Position);
gl.vertexAttribPointer( this.a_PointSize, 1, gl.FLOAT, false, FSIZE * 5, FSIZE * 2 );
gl.enableVertexAttribArray(this.a_PointSize);
gl.vertexAttribPointer(this.a_TexCoord, 2, gl.FLOAT, false, FSIZE * 5, FSIZE * 3);
gl.enableVertexAttribArray(this.a_TexCoord);

3.配置加载纹理

  1. 创建纹理对象。纹理对象用来管理WebGL系统中的纹理。
1
2
// Create a texture object
let texture = gl.createTexture();
  1. 加载纹理图像。使用Image对象
    1
    2
    3
    4
    5
    6
    7
    8
    // Create the image object
    let image = new Image();
    // Register the event handler to be called on loading an image
    image.onload = () => {
    loadTexture(gl, texture, u_Sampler, image);
    };
    // Tell the browser to load an image
    image.src = require("@/assets/sky.jpg");
  2. 配置纹理。(loadTexture())

图像Y轴反转 WebGL中st坐标系统与PNG,BMP,JPG等图片格式坐标系统y轴相反。

WebGLRenderingContext.pixelStorei()

WebGL_TexCoord
Image_Coord

激活纹理单元(texture unit)

WebGL使用纹理单元来同时使用多个纹理。每个纹理单元有一个单元编号,内置变量gl.TEXTURE0 gl.TEXTURE1 gl.TEXTURE2 ...各表示一个纹理单元。

WebGLRenderingContext.activeTexture()

绑定纹理对象

同缓冲区很像,对缓冲区操作前,要先绑定缓冲区对象至一个target上。对纹理对象操作前需要绑定到纹理绑定点(目标)(gl.TEXTURE_2D)。没法直接操作纹理对象,必须通过将纹理对象绑定到纹理单元上,然后操作纹理单元操作纹理对象

WebGLRenderingContext.bindTexture()

该方法完成俩任务:开启纹理对象,以及将纹理对象绑定到纹理单元上

配置纹理对象参数

设置纹理图像映射到图形上的方式。1.如何根据纹理坐标获取纹素。2.按那种方式重复。

WebGLRenderingContext.texParameterfi

将图像分配给纹理对象

示例使用JPG格式纹理图片,该格式像素使用RGB三个分量表示。texImage2D方法将图像存储在WebGL的纹理对象中。通过internalformat参数告诉WebGL纹理图像格式类型。internalformat必须与format指定纹理图像数据格式一致。

WebGLRenderingContext.texImage2D()

将纹理单元传递给片元着色器

通过第二个参数指定纹理单元编号(gl.TEXTURE0中的0)将纹理对象传给u_Sampler。

WebGLRenderingContext.uniform[1234][fi][v]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
loadTexture(gl, texture, u_Sampler, image) {
// Flip the image's y axis
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
// Enable texture unit0
gl.activeTexture(gl.TEXTURE0);
// Bind the texture object to the target
gl.bindTexture(gl.TEXTURE_2D, texture);
// Set the texture parameters
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// Set the texture image
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);
// Set the texture unit 0 to the sampler
gl.uniform1i(u_Sampler, 0);
}

WebGL 图元装配到光栅化与varying变量

图元装配过程(primitive assembly process)

几何图形装配(geometric shape assembly)

1.声明attribute vec4 a_Color 接受数据

2.赋值给varying vec4 v_Color

3.片元着色器中声明varying vec4 v_Color

如果顶点着色器中有类型和命名相同的varying变量,那么顶点着色器赋给该变量的值就会被自动传入片元着色器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let VSHADER_SOURCE = 
"attribute vec4 a_Position;\n" +
"attribute float a_PointSize;\n" +
"attribute vec4 a_Color;\n" +
"varying vec4 v_Color;\n" +
// varying variable
"uniform mat4 u_ModelMatrix;\n" +
"void main() {\n" +
" gl_Position = u_ModelMatrix * a_Position;\n" +
" gl_PointSize = a_PointSize;\n" +
" v_Color = a_Color;\n" +
// Pass the data to the fragment shader
"}\n"
let FSHADER_SOURCE =
"precision mediump float;\n" +
"uniform vec4 u_FragColor;\n" +
"varying vec4 v_Color;\n" +
// Receive the data from the vertex shader
"void main() {\n" +
" gl_FragColor = v_Color;\n" +
"}\n",

varying变量的作用和内插过程

varying变量。只能是float、vec2、vec3、vec4、mat2、mat3、mat4。必须全局。最少支持8个。
顶点着色器中的v_Color在传入片元着色器之前经过了内插过程。WebGL根据我们传入的颜色值,自动计算出所有片元的颜色,赋值给片元着色器中的v_Color

图元装配到光栅化

绘制过程

  1. 缓冲区对象中第一个坐标传递给a_Position继而被赋值给gl_Position,赋值后改顶点进入图形装配区,并储存。
  2. 重复执行顶点着色器,将所有坐标点传入并存储在装配区。
  3. 装配图形。根据gl.drawArrays()第一个参数决定如何装配顶点。
  4. 光栅化。将装配好的图元转化为片元(像素)。
  5. 光栅化结束后,逐片元调用片元着色器。每次处理一个片元,计算出该片元颜色,写入颜色缓冲区。直到所有片元被处理完,浏览器显示出结果。

计算点到线段最短距离——矢量法

一、思路

矢量算法过程清晰,如果具有一定的空间几何基础,则是解决此类问题时应优先考虑>的方法。当需要计算的数据量很大时,这种方式优势明显。

由于矢量具有方向性,故一些方向的判断直接根据其正负号就可以得知,使得其中的一些问题得以很简单的解决。

根据下图,可以看到,我们只需计算矢量AC)将其与矢量AB做比较即可分出以下结果。
矢量法示意

二、步骤

矢量AP矢量AB的单位向量,即可得到AC的长度值;
APAB
AC的模长与AB的单位向量相乘可以构成AC向量,所以容易得到:
矢量法3
最后,根据其正负以及大小可以判断出三种情况:
矢量法4

三、代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Segment
{
private Point pt1;
private Point pt2;
private double length;
public Segment(Point pt1, Point pt2)
{
this.pt1 = pt1;
this.pt2 = pt2;
double dx = Math.Abs(pt1.X - pt2.X);
double dy = Math.Abs(pt1.Y - pt2.Y);
this.length = Math.Sqrt(Math.Pow(dx, 2) + Math.Pow(dy, 2));
}
/// <summary>
/// Finding the shortest distance from a point to the line segment by vector method(矢量法)
/// </summary>
/// <param name="p"></param>
/// <returns></returns>
public double Dis2Pt(Point p)
{
//两向量点乘 P:p A:pt1 B:pt2 C:垂足
//矢量法
//不存在垂足C,求与A点距离
double APAB = (pt2.X - pt1.X) * (p.X - pt1.X) + (pt2.Y - pt1.Y) * (p.Y - pt1.Y);
if (APAB <= 0) { return Math.Sqrt(Math.Pow((p.X - pt1.X), 2) + Math.Pow((p.Y - pt1.Y), 2)); }
//不存在垂足C,求与B点距离
double AB2 = Math.Pow(length, 2);
if (APAB >= AB2) { return Math.Sqrt(Math.Pow((p.X - pt2.X), 2) + Math.Pow((p.Y - pt2.Y), 2)); }
//存在垂足C
double r = APAB / AB2;
double Cx = pt1.X + (pt2.X - pt1.X) * r;
double Cy = pt1.Y + (pt2.Y - pt1.Y) * r;
return Math.Sqrt(Math.Pow((p.X - Cx), 2) + Math.Pow((p.Y - Cy), 2));
}
}

1.行列号编号规则

​ 在讲瓦片的时候说到过这些规则,这里重复一遍:

​ 要在浏览器上把每个切片放到正确的位置,保证拼接正确,就要将每个瓦片进行编号,有了编号后就知道每个瓦片对应加载的位置,此处可以脑补拼图。下面先了解下编号的规则。

谷歌XYZ:Z表示缩放层级,Z=zoom;XY的原点在左上角,X从左向右,Y从上向下。

TMS:开源产品的标准,Z的定义与谷歌相同;XY的原点在左下角,X从左向右,Y从下向上。

QuadTree:微软Bing地图使用的编码规范,Z的定义与谷歌相同,同一层级的瓦片不用XY两个维度表示,而只用一个整数表示,该整数服从四叉树编码规则

百度XYZ:Z从1开始,在最高级就把地图分为四块瓦片;XY的原点在经度为0纬度位0的位置,X从左向右,Y从下向上。

瓦片原理图3

2.经纬度和行列号如何换算

​ 我们知道加载瓦片需要通过相应的URL请求到对应的图片并加载至浏览器正确的位置上。通常我们使用的地图API(如ArcGIS API for JS)会帮我们把参数计算出来,我们只要拼接成正确的URL就可以加载瓦片图层了。

​ 但是,如果我们想要自己爬取下载网上一些地图的瓦片,或者自己撸一个加载瓦片的方法,就要必须知道如何经纬度转换成瓦片的行列号

​ 先补一个。下列公式定义在使用墨卡托投影的地图中,从纬线φ和经线λ如何推导为坐标系中的点坐标x和y。墨卡托投影法

XYto84

84toXY

1)OpenStreetMap

特性1:z: [0~18] x,y: [0~(2^z-1)]

特性2:第z级别,x,y方向的瓦片个数均为:2^z

特性3:图片(z,x,y)像素(m,n)[注:像素坐标以左上角为原点,x轴向右,y轴向下]的经纬度[单位:度]分别为:

osmBLtoXY1

osmBLtoXY1

osmBLtoXY

2)Google Map

特性1:z: [0~18] x,y: [0~(2^z-1)]

特性2:图片(x,y,z)像素(m,n)[注:像素坐标以左上角为原点,x轴向右,y轴向下]的经纬度[单位:度]与openmapstreet方法一致。

3)Bing Map

瓦片(Tile)地图原理与加载

1. 什么是瓦片地图

​ 我们在网上浏览地图比如常见的:高德、谷歌、Bing、腾讯地图、百度地图、OpenStreetMap。地图加载时是一个方块一个方块加载显示出来。这种地图加载方式就是将地图(就是张图片)按照详细程度等级制作成各个等级的瓦片(还是张图片),结构如下图。根据用户缩放情况 显示不同等级的地图图片。

一些基本特性:

  1. 参考椭球:WGS84

  2. 投影:墨卡托投影Web_Mercator

  3. 当经度范围在[-180,180],投影为正方形时,纬度范围:[-85.05113, 85.05113]

  4. 图片大小:256*256

  5. 图片格式:jpg[有损压缩率高、不透明] png[无损、透明]

瓦片原理图1

2. 为什么使用瓦片地图

​ 1)瓦片地图缓存非常高效。如果你曾查看中央公园的地图而下载过曼哈顿的瓦片,当你需要显示泽西城的地图时,你的浏览器可以使用之前缓存的相同的瓦片,而不是重新再下载一次。

​ 2)瓦片地图可以渐进加载。即使当前地图的边缘部分还没有加载完成,也可以缩放移动到其他地方。

​ 3)瓦片地图简单易用。描述地图瓦片的坐标系统很简单,使得很容易在服务器、网络、桌面或移动设备上实现技术集成。

​ 4)传输效率高、减少服务器压力。由于地图内容会跟着用户缩放程度进行一定的简化和隐藏,这一切如果动态由服务器计算后返回,效率大大降低,而预先为地图分好固定等级,将地图作为图片存储在服务端,更据请求只返回特定的地图图片则简便高效很多。

3. 瓦片地图怎么做出来的

​ 通常找软件生成去吧。。。基本原理过程就是:

瓦片原理图2

第一点:地球是个近似球体。要转为平面的地图需要投影。或者直接使用正射影像当瓦片用咯。

第二点:投影完后,就按照地图详细等级进行切分。最高级(level = 0),需要的信息最少,最宏观,就是一张256*256像素图片,下一级(level = 1)精细一些,就是512*512像素,以此类推。最后就成了个金字塔的体系。

第三点:每张图片都是同样大小的图片,通常会都切分为256*256大小的图片,这就是瓦片了。算一算就知道,(level = 0)一张瓦片,到(level = 1)四张瓦片……..

4. 瓦片怎么加载到正确的位置的

​ 要在浏览器上把每个切片放到正确的位置,保证拼接正确,就要将每个瓦片进行编号,有了编号后就知道每个瓦片对应加载的位置,此处可以脑补拼图。下面先了解下编号的规则。

谷歌XYZ:Z表示缩放层级,Z=zoom;XY的原点在左上角,X从左向右,Y从上向下。

TMS:开源产品的标准,Z的定义与谷歌相同;XY的原点在左下角,X从左向右,Y从下向上。

QuadTree:微软Bing地图使用的编码规范,Z的定义与谷歌相同,同一层级的瓦片不用XY两个维度表示,而只用一个整数表示,该整数服从四叉树编码规则

百度XYZ:Z从1开始,在最高级就把地图分为四块瓦片;XY的原点在经度为0纬度位0的位置,X从左向右,Y从下向上。

瓦片原理图3

​ 编好了号,瓦片做好了,就是一堆文件夹里放着图片。直观点就是下图:

​ 文件夹名字表示级别: 瓦片原理图4

​ 点开21级文件夹,文件夹名表示x:瓦片原理图5

​ 再打开文件夹就是图片了,名字就是y:瓦片原理图6

​ *.kml文件里面存了写坐标信息有需要可以读取,这里先不管它。

5.瓦片的加载方法

​ 有了瓦片,再简单的说下加载瓦片地图的基本思路

​ 1. 要根据用户缩放程度算出显示范围所对应的瓦片的x、y、z(leve)(也可以说是行列号)。

​ 2. 接下来就再对应的位置,获取并加载上对应的x、y、z的瓦片。

​ 3. 必要的一些缩放功能,平移等等基本的地图浏览操作。

​ 上面说的很简单,但是要自己实现的话还是很麻烦的对吧。所以上面这些,基本都有现成JS库帮我们去完成。而瓦片无特殊要求的话,去获取,高德、谷歌、OSM、腾讯等等都可以很方便。下面是简单的加载天地图瓦片地图服务的例子,使用的ArcGIS API for JavaScript,官方教程也很多可以看看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var MyCustomTileLayer = BaseTileLayer.createSubclass({
// properties of the custom tile layer
properties: {
urlTemplate: null,
},
// override getTileUrl()
// generate the tile url for a given level, row and column
getTileUrl: function (level, row, col) {
return this.urlTemplate.replace("{z}", level).replace("{x}", col).replace("{y}", row).replace("{tag}", col % 8);
}
});
var TDMap = new MyCustomTileLayer({
urlTemplate: "http://t{tag}.tianditu.gov.cn/DataServer?T=vec_w&x={x}&y={y}&l={z}",
tint: "#71DE6E", // blue color
title: "TD Map",
});
var myMap = new Map({
layers: [TDMap]
});
var view = new SceneView({
container: "viewDiv",
map: myMap,
});

不太明白的看看这个:

​ 我们天地图官网上打开地图时候F12里找到的请求:参数就有x、y、l(leve),再看上面的代码getTileUrl函数,帮我们把算出来的三个参数替换到url中去请求到对应的瓦片,tag是天地图有好几个瓦片服务地址任意取一个数,暂时可以不管。

瓦片原理图6

​ url搞定了剩下的就是造个layer造个map加到view中。

6. 加载自定义的瓦片

​ 上面我们用的是天地图的瓦片,我们很多时候需要加载自己的做好的瓦片,现在我们以上面截图的那些做好的瓦片文件为例,写个属于自己的瓦片地图的服务:

这里用.NET Web API实现

其实思路简单:

  1. 按照x,y,z去得到对应瓦片的文件路径,x、y、z是url里传过来的。

  2. 将获取到的图片返回

Modles:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public string getImage(string mt, string version, string x, string y, string z)
{
try
{
//由于采用的瓦片编号为TMS
//而我们使用ArcGIS JS API来加载 其默认为Google瓦片编码规则
//所以这里将y值进行简单换算
y = (Math.Pow(2, Convert.ToInt32(z)) - Convert.ToInt32(y) - 1).ToString();

//取对应路径下的图片
string currentPath = System.Web.Hosting.HostingEnvironment.MapPath("~/") + @"tiles";
if (File.Exists(currentPath + "/" + mt + "/" + version + "/" + z + "/" + x + "/" + y + ".png"))
{
imgpath = currentPath + "/" + mt + "/" + version + "/" + z + "/" + x + "/" + y + ".png";
}
else
{
imgpath = currentPath + "/Northophoto.png";
}
return imgpath;
}
catch (Exception ex)
{
return null;
}
}
Controller:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class testController : ApiController
{
[HttpGet]
//主要就是xyz 其他自定义的参数,可以不用管
public HttpResponseMessage Foo(string mt, string version, string x, string y, string z)
{
try
{
TestClass modclass = new TestClass();
var imgPath = modclass.getImage(mt, version, x, y, z);
FileStream fs = new FileStream(imgPath, FileMode.Open);
HttpResponseMessage resp = new HttpResponseMessage(HttpStatusCode.OK);
resp.Content = new StreamContent(fs);
resp.Content.Headers.ContentType = new MediaTypeHeaderValue("image/jpg");
return resp;
}
catch (Exception)
{
return null;
}
}
}

​ 这样前端调用相应的URL并带上的相应的参数 具体xyz参数值由ArcGIS JS API得出

> ps:具体怎么算出来的看下一篇文章。

​ 下面我们看看简单的使用ArcGIS JS API加载瓦片的方法,和上面加载天地图差不多,但是参数使要根据我们上一步定义的参数来传递,具体也可以去看看Esri的官方API文档:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var MyCustomTileLayer = BaseTileLayer.createSubclass({
// properties of the custom tile layer
properties: {
urlTemplate: null,
},
// override getTileUrl()
// generate the tile url for a given level, row and column
getTileUrl: function (level, row, col) {
return this.urlTemplate.replace("{z}", level).replace("{x}", col).replace("{y}", row).replace("{tag}", col % 8);
}
});

var Tile = new MyCustomTileLayer({
urlTemplate: "http://localhost:62881/api/test?mt=测试&version=20181210&x={x}&y={y}&z={z}",
title: "Tile",
});

​ 这样底图图层就创建出来了,接下来就可以直接使用:

瓦片原理图7