更新于 

附录D:左手还是右手坐标系

官方回答:

The GL does not force left-or right-handedness on of its coordinate systems.

翻译老是问这种钻牛角尖的问题,都说了左右手都行。

那为什么之前在WebGL中频频使用右手坐标系呢?

这个答案和你问南方人为什么吃肉粽子,和北方人为什么吃甜粽子一样,
都是因为传统

CoordinateSystem.js

WebGL的z轴指向屏幕外,也就是说:

  • 越深的物体z值越小
  • 越浅的物体z值越大
CoordinateSystem
CoordinateSystem运行结果
CoordinateSystem运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let VSHADER_SOURCE = `
attribute vec4 a_Position;
attribute vec4 a_Color;
varying vec4 v_Color;
void main(){
gl_Position = a_Position;
v_Color = a_Color;
}
`

let FSHADER_SOURCE = `
#ifdef GL_ES
precision mediump float;
#endif
varying vec4 v_Color;
void main(){
gl_FragColor = v_Color;
}
`
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function main() { 
let canvas = document.getElementById('webgl')
let gl = getWebGLContext(canvas)
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('failed to init shaders')
return
}
gl.clearColor(0, 0, 0, 1)

let n = initVertexBuffers(gl)
if (n < 0) {
console.log('failed to init vertex buffers')
return
}
gl.clear(gl.COLOR_BUFFER_BIT)
gl.drawArrays(gl.TRIANGLES, 0, n)
}
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
function initVertexBuffers(gl) {
let vertices = new Float32Array([
// bluetriangle
0.0, 0.6, -0.1, 0.0, 0.0, 1.0,
-0.5, -0.5, -0.1, 0.4, 0.4, 0.4,
0.5, -0.5, -0.1, 0.0, 0.0, 1.0,

// green triangle
-0.5, 0.5, -0.5, 0.0, 1.0, 0.0,
0.5, 0.5, -0.5, 0.4, 0.4, 0.4,
0.0, -0.6,-0.5, 0.0, 1.0, 0.0
])
let FSIZE = vertices.BYTES_PER_ELEMENT
let n = 6
let a_Position = gl.getAttribLocation(gl.program, 'a_Position')
let a_Color = gl.getAttribLocation(gl.program, 'a_Color')
let buffer = gl.createBuffer()
if (!buffer) {
return -1
}
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW)
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, 6*FSIZE, 0)
gl.enableVertexAttribArray(a_Position)
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, 6*FSIZE, 3*FSIZE)
gl.enableVertexAttribArray(a_Color)

return n
}

隐藏面消除和裁剪坐标系统

为什么结果会是绿色三角形在前?

蓝色三角形数据

1
2
3
0.0, 0.6, -0.1, 0.0, 0.0, 1.0, 
-0.5, -0.5, -0.1, 0.4, 0.4, 0.4,
0.5, -0.5, -0.1, 0.0, 0.0, 1.0,

绿色三角形数据

1
2
3
-0.5, 0.5, -0.5, 0.0, 1.0, 0.0,
0.5, 0.5, -0.5, 0.4, 0.4, 0.4,
0.0, -0.6,-0.5, 0.0, 1.0, 0.0

这是因为没有进行隐藏面消除
这时WebGL默认是按照绘制顺序来排列深度的

进行隐藏面消除之后的结果

打开隐藏面消除

1
2
3
gl.enable(gl.DEPTH_TEST)
// ...
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
绘制结果会改变吗?

答: 并没有哦

依旧是绿色三角形在上
依旧是绿色三角形在上

这是因为 WebGL使用的是左手坐标系
z轴是指向屏幕内部

左手坐标系
左手坐标系

裁剪坐标系和可视空间

裁剪坐标系(clip coordinate system)

开启隐藏面消除的时候会用到裁剪坐标系,
裁剪坐标系本身就是左手坐标系

到底要怎样才能让WebGL恢复右手坐标系的状态呢?
先要了解为什么开启消除面隐藏后还是以左手坐标系绘图

:正因为裁剪坐标系左手坐标系
所以 a_Position 的数值传递给 gl_Position 时,
会自动被纳入 左手坐标系

解决方法

只有设定可视空间
隐藏面消除才能够正常工作

关键就是设定可视空间时会指定:

  • 近剪裁面 near
  • 远剪裁面 far

这两者决定了使用的坐标系:

  • near < far
    • z轴指向屏幕外
    • 右手坐标系
  • near > far
    • z轴指向屏幕里
    • 左手坐标系

给CoordinateSystem.js添加上可视空间的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let VSHADER_SOURCE = `
...
uniform mat4 u_MvpMatrix;
...
gl_Position = u_MvpMatrix * a_Position;
...
`
function main(){
...
let mvpMatrix = new Matrix4()
mvpMatrix.setOrtho(
-1, 1, // left right
-1, 1, // bottom top
-1, 1 // near far
)
gl.uniformMatrix4fv(u_MvpMatrix,false,mvpMatrix.elements)
...
}
右手坐标系下的绘制结果
右手坐标系下的绘制结果

可视空间、投影矩阵、坐标系

为什么near

这里需要考虑一下 setOrtho 对应的投影矩阵,

1
mvpMatrix.setOrtho(-1,1,-1,1,-1,1)

left = -1
right = 1
bottom = -1
top = 1
near = -1
far = 1

对应的投影矩阵应该是

1, 0, 0, 0,
0, 1, 0, 0,
0, 0, -1, 0,
0, 0, 0, 1,

相当于对z轴进行翻转,

setScale(1,1,-1)

由于裁剪坐标系本身就是左手坐标系,
这一操作相当于将左手左边系翻转成右手坐标系。

总结

总之这一切的一切都是无中生有

WebGL默认行为

规定是 左手坐标系
比如裁剪面一类的默认行为

大部分WebGL库和程序

都采用的是 右手坐标系
别问,问就是传统

为了解决这个冲突

需要将使用 右手坐标系的WebGL库,
通过规定 可视空间 的方式翻转z轴,
从而能够实现一些规定使用 左手坐标系的默认行为

总结

总之这个问题告诉我们程序员奇奇怪怪的习惯如何能无中生有出一个深奥晦涩的技术问题,
还告诉我们让程序员统一开发规范有多难