webgl用法(第二章)

点击画布绘制点

上例只是实现了一个静态点的绘制,但真正的 WebGL 应用是需要通过网页和用户进行交互,从而改变画面。实现一个简单交互程序:在画布上,鼠标点击的位置上绘制一个点,且这个点的颜色是随机的。
要求:通过 JavaScript 往着色器程序中传入顶点位置和颜色数据,从而绘制点的位置和颜色。

着色器程序

1、顶点着色器
<!-- 顶点着色器 -->
<script type="shader-source" id="vertexShader">
  // 浮点数设置为中等精度
  precision mediump float;

  // 接收 JavaScript 传递过来的点的坐标(X, Y);包含 2 个浮点元素的容器类型
  attribute vec2 a_Position;

  // 接收 canvas 的尺寸;包含 2 个浮点元素的容器类型
  attribute vec2 a_Screen_Size;

  void main () {
    // 将 canvas 的坐标值转换为 [-1.0, 1.0] 的范围
    // 即:从 canvas 坐标系转变到 NDC 坐标系(设备坐标系)
    vec2 position = (a_Position / a_Screen_Size) * 2.0 - 1.0;
    
    // canvas 的 Y 轴坐标方向和设备坐标系相反
    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 个浮点数
// 从 canvas 坐标系转变到 NDC 坐标系
vec2 position = (a_Position / a_Screen_Size) * 2.0 - 1.0

// canvas 的 Y 轴坐标方向和设备坐标系相反
position = position * vec2(1.0, -1.0);

// 最终的顶点坐标
// vec4(x, y, z, w)
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;

  // 全局变量,用来接收 JavaScript 传递过来的颜色;包含 4 个浮点元素的容器类型
  uniform vec4 u_Color;

  void main () {
    // 将颜色处理成 GLSL 允许的范围 [0, 1]
    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>
  // 获取 canvas
  const canvas = getDom('#canvas');

  // 设置 canvas 尺寸为满屏
  resizeCanvas(canvas);

  // 获取 webgl 绘图上下文
  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);

  // 告诉 WebGL 运行哪个着色器程序
  gl.useProgram(program);

  // 获取顶点着色器中的变量 a_Position 的指针位置
  const a_Position = gl.getAttribLocation(program, 'a_Position');

  // 获取顶点着色器中的变量 a_Screen_Size 的指针位置
  const a_Screen_Size = gl.getAttribLocation(program, 'a_Screen_Size');

  // 获取片元着色器中的变量 u_Color 的指针位置
  const u_Color = gl.getUniformLocation(program, 'u_Color');

  // 向顶点着色器的 a_Screen_Size 传递 canvas 尺寸信息
  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

/**
 * 获取随机颜色
 * @returns { Object } 颜色对象
 */
function randomColor() {
  return {
    r: random() * 255,
    g: random() * 255,
    b: random() * 255,
    a: random() * 1
  }
}

/**
 * 获取 DOM 元素
 * @param { String } id - id 属性值
 * @return { HTMLElement } HTML 元素
 */
function getDom(id) {
  let selector = ''
  if (id.startsWith('#')) {
    selector = id
  } else {
    selector = `#${id}`
  }
  return document.querySelector(selector)
}

/**
 * 设置画布大小
 * @param { HTMlElement } canvas - canvas DOM 元素
 * @param { Number } width - 宽度
 * @param { Number } height - 高度
 */
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
  }
}

/**
 * 获取 WebGL 上下文
 * @param { HTMlElement } canvas - canvas DOM 元素
 * @return { WebGLRenderingContext } WebGL 绘图上下文
 */
function getWebGLContext (canvas) {
  return canvas.getContext('webgl') || canvas.getContext('experimental-webgl')
}

/**
 * 创建着色器
 * @param { WebGLRenderingContext } gl - WebGL 上下文
 * @param { String } type - 着色器类型:参数为 gl.VERTEX_SHADER(顶点着色器) 或 gl.FRAGMENT_SHADER(片元着色器) 两者中的一个
 * @param { String } source - GLSL 代码
 * @return { WebGLShader } 返回顶点或片元着色器对象
 */
function createShader (gl, type, source) {
  // 创建着色器对象
  const shader = gl.createShader(type)

  // 设置着色器的 GLSL 程序代码
  gl.shaderSource(shader, source)

  // 编译着色器
  gl.compileShader(shader)

  // 检测是否编译正常
  const result = gl.getShaderParameter(shader, gl.COMPILE_STATUS)

  if (result) {
    return shader
  }

  // 删除着色器对象
  gl.deleteShader(shader)
}

/**
 * 从 Script 标签中的 GLSL 代码创建着色器
 * @param { WebGLRenderingContext } gl - WebGL 上下文
 * @param { String } type - 着色器类型:参数为 gl.VERTEX_SHADER(顶点着色器) 或 gl.FRAGMENT_SHADER(片元着色器) 两者中的一个
 * @param { String } scriptId - 包含 GLSL 代码的 script 元素 ID
 * @return { WebGLShader } 着色器对象
 */
function createShaderFromScript(gl, type, scriptId) {
  const sourceScript = getDom(scriptId)

  if (!sourceScript) {
    return null
  }

  return createShader(gl, type, sourceScript.innerHTML)
}

/**
 * 创建着色器程序
 * @param { WebGLRenderingContext } gl - WebGL 上下文
 * @param { WebGLShader } vertexShader - 顶点着色器对象
 * @param { WebGLShader } fragmentShader - 片元着色器对象
 * @return { WebGLProgram } 着色器程序
 */
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 并行计算优势对所有顶点执行变换。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值