🚿 [WebGL编程指南] 2. 入门
着色器是什么
要使用WbGL进行绘图就必须使用着色器。

WebGL坐标系
由于WbGL处理的是三维图形,所以它使用三维坐标系统(笛卡尔坐标系),具有 X轴、Y轴和Z轴。三维坐标系统很容易理解,因为我们的世界也是三维的:具有宽度、高度和长度。在任何坐标系统中,轴的方向都非常重要。通常,在WbGL中,当你面向计算机屏幕时,X轴是水平的(正方向为右),Y轴是垂直的(正方向为下),而Z轴垂直于屏幕(正方向为外)

WebGL的坐标系和<canvas>绘图区的坐标系不同,需要将前者映射到后者。默认情况下,如图2.18所示,WebGL坐标与<canvas>坐标的对应关系如下。

绘制一个点
<html charset="utf-8" lang="zh">
<head>
<title>WebGLDemo</title>
</head>
<body>
<canvas id="webgl" height="300" width="300"></canvas>
<br />
<button id="draw-point-button">绘制一个点</button>
<script src="./src/drawPoint.js"></script>
<script src="./utils/webGL.utils.js"></script>
<script>
const drawPonitButton = document.querySelector("#draw-point-button");
if (drawPonitButton) {
drawPonitButton.addEventListener("click", () => {
drawPoint();
});
}
</script>
</body>
</html>
着色器使用类似于C的OpenGL ES着色器语言(GLSL ES)来编写。和C语言程序一样,必须包含一个main()函数。 main()前面的关键字void表示这个函数不会有返回值。还有,你不能为main()指定参数。
注意,g1_Position变量必须被赋值,否则着色器就无法正常工作。相反,g1_Pointsize并不是必须的,如果你不赋值,着色器就会为其取默认值1.0。
注意,赋给g1_Position的矢量中,我们添加了1.0作为第4个分量。由4个分量组成的矢量被称为齐次坐标因为它能够提高处理三维数据的效率
齐次坐标使用如下的符号描述:(x,y,z,w)。齐次坐标(x,y,z,w)等价于三维坐标(x/w,y/w,z/w)所以如果齐次坐标的第4个分量是1.0,你就可以将它当做三维坐标来使用。w的值必须是大于等于0的。如果w趋近于0,那么它所表示的点将趋近无穷远,所以在齐次坐标系中可以有无穷概念。齐次坐标的存在,使得用矩阵乘法来描述顶点变换成为可能,三维图形统在计算过程中,通常使用齐次坐标来表示顶点的三维坐标。
gl.drawArrays()是一个强大的函数,它可以用来绘制各种图形,该函数的规范如下表所示。

// 入口函数
function drawPoint() {
// 顶点着色器
const VSHADER_SOURCE =
"void main() {\n" +
" gl_Position = vec4(0.0, 0.0, 0.0, 1.0);\n" + // 设置坐标
" gl_PointSize = 10.0;\n" + // 设置大小
"}\n";
// 片元着色器
const FSHADER_SOURCE =
"void main() {\n" +
" gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n" + // 设置颜色
"}\n";
var canvas = document.getElementById("webgl");
var gl = canvas.getContext("webgl");
// 初始化着色器
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log("Failed to intialize shaders.");
return;
}
// Specify the color for clearing <canvas>
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// 清除缓冲区
gl.clear(gl.COLOR_BUFFER_BIT);
// 绘制
gl.drawArrays(gl.POINTS, 0, 1);
}

initShader
function initShaders(gl, vshader, fshader) {
const vertextShader = loadShader(gl, gl.VERTEX_SHADER, vshader)
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, vshader)
// 创建程序对象
const program = gl.createProgram()
// 为程序分配着色器对象
gl.attachShader(program, vertextShader)
gl.attachShader(program, fragmentShader)
// 连接着色器
gl.linkProgram(program)
// 检查连接
const linked = gl.getProgramParameter(program, gl.LINK_STATUS)
// ...
gl.useProgram(program)
gl.program = program
}
function loadShader(gl, type, source) {
const shader = gl.createShader(type)
// 设置着色器源代码
gl.shaderSource(shader, source)
// 编译着色器
gl.compileSource(shader)
// 检查着色器编译状态
const compiled = gl.getShaderParameter(sharder, gl.COMPILE_STATUS)
// ...
return shader
}


GLSL ES语言需要编译成二进制可执行格式,WebGL使用这种可执行格式。
编译错误可以通过gl.getShaderParameter(sharder, gl.COMPILE_STATUS) 来判断。错误信息可以通过gl.getShaderInfoLog获取。




传递变量
我们的目标是,将位置信息从JavaScript程序中传给顶点着色器。有两种方式可以做到这点:attribute变量和uniform变量。使用哪一个变量取决于需传输的数据本身,attribute变量传输的是那些与顶点相关的数据,而uniform变量传输的是那些对于所有顶点都相同(或与顶点无关)的数据。本例将使用attribute变量来传输顶点坐标,显然不同的顶点通常具有不同的坐标。
// 入口函数
function drawPoint() {
// 顶点着色器
const VSHADER_SOURCE =
'attribute vec4 a_Position;\n' + // 使用attribute 变量
'void main() {\n' +
' gl_Position = a_Position;\n' +
' gl_PointSize = 10.0;\n' +
'}\n';
// ...
// 初始化着色器
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log("Failed to intialize shaders.");
return;
}
// 获取 a_Position 储存位置
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
// 传递变量
gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0);
// ...
}
通过鼠标点击绘制
function drawPoint() {
// ...
// 获取 a_Position 储存位置
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
canvas.onmounsedown = (ev) => {
click(ev, gl, canvas, a_Position)
}
// ...
// 清除缓冲区
gl.clear(gl.COLOR_BUFFER_BIT);
}
let points = []
function click(ev, gl, canvas, a_Position) {
let x = ev.clientX // 鼠标点击x
let y = ev.clientY // 鼠标点击y
let rect = ev.target.getBoundingClientRect();
x = ((x - rect.left) - canvas.width / 2) / (canvas.width / 2)
y = (canvas.height / 2 - (y - rect.top)) / (canvas.height / 2)
points.push(x)
points.push(y)
// 清除缓冲区
gl.clear(gl.COLOR_BUFFER_BIT);
for(let i = 0; i < points.length; i+=2 ) {
// 传递变量
gl.vertexAttrib3f(a_Position, points[i], points[i+1], 0.0);
// 绘制
gl.drawArrays(gl.POINTS, 0, 1);
}
}

改变颜色
// 入口函数
function drawPoint() {
// 片元着色器
const FSHADER_SOURCE =
"uniform vec4 u_FragColor; \n+"
"void main() {\n" +
" gl_FragColor = u_FragColor;\n" + // 设置颜色
"}\n";
// 获取u_FragColor变量储存位置
const u_FragColor = gl.getUniformLocation(gl.program, 'u_FragColor');
// 设置RGBA颜色
gl.uniform4f(u_FragColor, 1.0, 0.0, 0.0, 0.0)
// ...
}