点击画布绘制点
上例只是实现了一个静态点的绘制,但真正的 WebGL 应用是需要通过网页和用户进行交互,从而改变画面。实现一个简单交互程序:在画布上,鼠标点击的位置上绘制一个点,且这个点的颜色是随机的。
要求:通过 JavaScript 往着色器程序中传入顶点位置和颜色数据,从而绘制点的位置和颜色。
着色器程序
1、顶点着色器
<!-- 顶点着色器 -->
<script type="shader-source" id="vertexShader">
precision mediump float;
attribute vec2 a_Position;
attribute vec2 a_Screen_Size;
void main () {
vec2 position = (a_Position / a_Screen_Size) * 2.0 - 1.0;
position = position * vec2(1.0, -1.0);
gl_Position = vec4(position, 0.0, 1.0);
gl_PointSize = 10.0;
}
</script>
代码解析
定义两个 attribute 变量: a_Position 和 a_Screen_Size
a_Position 变量接收 JavaScript 传递过来的 canvas 坐标系下的点击坐标(顶点的 x 坐标和 y 坐标)
a_Screen_Size 变量接收 JavaScript 传递过来的 canvas 的宽高尺寸
vec2 代表 2 维向量容器,可以存储 2 个浮点数
vec2 position = (a_Position / a_Screen_Size) * 2.0 - 1.0
position = position * vec2(1.0, -1.0);
gl_Position = vec4(position, 0.0, 1.0);
上面这句代码用来将浏览器窗口坐标转换成裁剪坐标,之后通过透视除法,除以 w 值(此处为 1)转换成设备坐标(NDC 坐标系)。
这个算法首先将 (x, y) 转化到 [0, 1] 区间,再将 [0, 1] 之间的值乘以 2 转化到 [0, 2] 区间,最后再减去 1,转化到 [-1, 1] 之间的值,即 NDC 坐标。
运算符:向量的对应位置计算,得到一个新的向量
vec * 浮点数
vec2(x, y) * 2.0 等价于 vec2(x * 2.0, y * 2.0)
vec * vec
vec2(x, y) * vec2(m, n) 等价于 vec2(x * m, y * n)
加减乘除规则基本一致
注意:参与运算的两个 vec 向量的维数必须相同
2、片元着色器
<!-- 片元着色器 -->
<script type="shader-source" id="fragmentShader">
precision mediump float;
uniform vec4 u_Color;
void main () {
vec4 color = u_Color / vec4(255, 255, 255, 1);
gl_FragColor = color;
}
</script>
代码分析
定义一个 uniform 变量(全局变量):u_Color
u_Color 变量接收 JavaScript 传递过来的随机颜色
precision
精度设置限定符。
使用此限定符设置完精度后,之后所有该数据类型都沿用该精度,除非再单独设置。
attribute 变量
只能在顶点着色器中定义。
uniform 变量
被 uniform 修饰的变量是一个全局变量。
既可以在顶点着色器中定义,也可以在片元着色器中定义。
varing 变量
用来从顶点着色器中往片元着色器传递数据。
利用它可以在顶点着色器中声明一个变量并对其赋值,经过插值处理后,在片元着色器中取出插值后的值来使用。
JavaScript 程序
动态绘制点逻辑
声明一个数组 points 存储点击位置坐标
绑定 canvas 点击事件
触发点击操作时,把点击坐标添加到数组 points 中
遍历每个点执行绘制操作
<!-- 场景设置 -->
<script>
const canvas = getDom('#canvas');
resizeCanvas(canvas);
const gl = getWebGLContext(canvas);
const vertexShader = createShaderFromScript(gl, gl.VERTEX_SHADER, 'vertexShader');
const fragmentShader = createShaderFromScript(gl, gl.FRAGMENT_SHADER, 'fragmentShader');
const program = createProgram(gl ,vertexShader, fragmentShader);
gl.useProgram(program);
const a_Position = gl.getAttribLocation(program, 'a_Position');
const a_Screen_Size = gl.getAttribLocation(program, 'a_Screen_Size');
const u_Color = gl.getUniformLocation(program, 'u_Color');
gl.vertexAttrib2f(a_Screen_Size, canvas.width, canvas.height);
const points = [];
canvas.addEventListener('click', e => {
const x = e.pageX;
const y = e.pageY;
const color = randomColor();
points.push({ x, y, color })
render(gl)
})
function render (gl) {
gl.clear(gl.COLOR_BUFFER_BIT);
points.forEach(point => {
const { x, y, color } = point
gl.vertexAttrib2f(a_Position, x, y)
gl.uniform4f(u_Color, color.r, color.g, color.b, color.a)
gl.drawArrays(gl.POINTS, 0, 1);
})
}
gl.clearColor(0, 0, 0, 1.0);
render(gl);
</script>
公共函数
const random = Math.random
function randomColor() {
return {
r: random() * 255,
g: random() * 255,
b: random() * 255,
a: random() * 1
}
}
function getDom(id) {
let selector = ''
if (id.startsWith('#')) {
selector = id
} else {
selector = `#${id}`
}
return document.querySelector(selector)
}
function resizeCanvas(canvas, width, height) {
if (canvas.width !== width) {
canvas.width = width ? width : window.innerWidth
}
if (canvas.height !== height) {
canvas.height = height ? height : window.innerHeight
}
}
function getWebGLContext (canvas) {
return canvas.getContext('webgl') || canvas.getContext('experimental-webgl')
}
function createShader (gl, type, source) {
const shader = gl.createShader(type)
gl.shaderSource(shader, source)
gl.compileShader(shader)
const result = gl.getShaderParameter(shader, gl.COMPILE_STATUS)
if (result) {
return shader
}
gl.deleteShader(shader)
}
function createShaderFromScript(gl, type, scriptId) {
const sourceScript = getDom(scriptId)
if (!sourceScript) {
return null
}
return createShader(gl, type, sourceScript.innerHTML)
}
function createProgram (gl, vertexShader, fragmentShader) {
const program = gl.createProgram()
vertexShader && gl.attachShader(program, vertexShader)
fragmentShader && gl.attachShader(program, fragmentShader)
gl.linkProgram(program)
const result = gl.getProgramParameter(program, gl.LINK_STATUS)
if (result) {
return program
}
const errorLog = gl.getProgramInfoLog(program)
gl.deleteProgram(program)
throw errorLog
}
所用知识点总结
GLSL
gl_Position
内置变量,用来设置顶点坐标。
gl_PointSize
内置变量,用来设置顶点大小。
vec2、vec4
vec2 - 2 维向量容器,可以存储 2 个浮点数。
vec4 - 4 维向量容器,可以存储 4 个浮点数。
gl_FragColor
内置变量,用来设置像素颜色。
precision
精度设置限定符。
使用此限定符设置完精度后,之后所有该数据类型都沿用该精度,除非再单独设置。
attribute 变量
只能在顶点着色器中定义。
uniform 变量
被 uniform 修饰的变量是一个全局变量。
既可以在顶点着色器中定义,也可以在片元着色器中定义。
varing 变量
用来从顶点着色器中往片元着色器传递数据。
利用它可以在顶点着色器中声明一个变量并对其赋值,经过插值处理后,在片元着色器中取出插值后的值来使用。
运算符
向量的对应位置进行运算,得到一个新的向量。
vec * 浮点数
vec2(x, y) * 2.0 等价于 vec2(x * 2.0, y * 2.0)
vec * vec
vec2(x, y) * vec2(m, n) 等价于 vec2(x * m, y * n)
加减乘除规则基本一致
注意:参与运算的两个 vec 向量的维数必须相同
JavaScript 程序连接着色器程序
createShader 创建着色器对象
创建着色器对象
shaderSource 提供着色器源码
提供着色器源码
compileShader 编译着色器对象
编译着色器对象
createProgram 创建着色器程序
创建着色器程序
attachShader 绑定着色器对象
绑定着色器对象
linkProgram 链接着色器程序
链接着色器程序
useProgram 启用着色器程序
启用着色器程序
JavaScript 传递数据给着色器
getAttribLocation 找 attribute 变量地址
找到着色器中的 attribute 变量地址
getUniformLocation 找 uniform 变量地址
找到着色器中的 uniform 变量地址
vertexAttrib2f 给 attribute 变量传递 2 个浮点数
给 attribute 变量传递两个浮点数
uniform4f 给 uniform 变量传递 4 个浮点数
给 uniform 变量传递四个浮点数
WebGL 图元
gl.POINTS 点图元
将绘制图元类型设置成点图元
WebGL 绘制函数
drawArrays 用指定图元进行绘制
用指定的图元进行绘制
注意
本例中的坐标转换实在着色器阶段完成的。
通常会在 JavaScript 上计算出转换矩阵,然后将转换矩阵连同顶点信息一并传递给着色器,利用 GPU 并行计算优势对所有顶点执行变换。