关于javascript:用-WebGL-做一个齿轮动画

37次阅读

共计 8782 个字符,预计需要花费 22 分钟才能阅读完成。

原文:Aral Roca

翻译:疯狂的技术宅

https://aralroca.com/blog/how…

未经容许严禁转载

本文持续“WebGL 的第一步”中的内容,上一篇文章中我讲述了 WebGL 是什么以及它是如何工作的,包含:shader、program、缓冲区、如何将数据从 CPU 链接到 GPU 和最终怎么渲染三角形。

在本文中,咱们将钻研如何渲染更简单的构造以及怎么使其静止。所以,咱们将实现三个 动静齿轮

辨认形态

要绘制的齿轮由 组成,不过这些圆须要一些变动:带齿的圆、带有黑白边框的圆和填充有色彩的圆。

咱们能够通过绘制圆来绘制这些齿轮,然而在 WebGL 中只能光栅化三角形,点和线 … 所以这些圆之间的区别是什么,怎样才能做到呢?

带边框的圆

咱们将应用多个 来绘制带边框的圆,:

填充色彩的圆

咱们将应用多个 三角形 绘制一个填充色彩的圆,:

所以须要用 进化三角形(Triangle strip)绘制模式:

进化三角形(Triangle strip) 是三角形网格中一系列相连的三角形,共享顶点,从而能够更无效地利用计算机图形的内存。它们比不带索引的三角列表更无效,但效率个别不如带索引的三角列表稳固。之所以应用进化三角形,次要起因是可能缩小创立一系列三角形所需的数据量。存储在内存中的顶点数量从 3N 缩小到了 N + 2,其中 N 是要绘制的三角形数量。这样能够缩小磁盘空间的应用,并可能使它们更快地加载到内存中。

带齿轮的圆

咱们还会应用 三角形 解决齿轮。这次不必“strip”模式,而是要绘制从圆周核心辐射开的三角形。

在构建齿轮时,还要在外部创立另外一个充斥色彩的圆,以便使齿轮从圆自身突出进去。

辨认要绘制的数据

这 3 种图形的共同点是能够从 2 个变量中计算出它们的坐标:

  1. 圆心(xy
  2. 半径

在上一篇文章中咱们晓得了,webGL 中的坐标范畴是从 -1 到 1。先让找到每个齿轮的核心及其半径:

此外还有一些特定数字的可选变量,例如:

  • 齿数
  • 笔触色彩(边框的色彩)*
  • 填充色
  • 子级(更多具备雷同数据结构的齿轮)
  • 旋转方向(仅对父级无效)

最初在 JavaScript 中,咱们将失去一个蕴含三个齿轮及其所有整机的数据的数组:

const x1 = 0.1
const y1 = -0.2

const x2 = -0.42
const y2 = 0.41

const x3 = 0.56
const y3 = 0.28

export const gears = [
  {center: [x1, y1],
    direction: 'counterclockwise',
    numberOfTeeth: 20,
    radius: 0.45,
    fillColor: [0.878, 0.878, 0.878],
    children: [
      {center: [x1, y1],
        radius: 0.4,
        strokeColor: [0.682, 0.682, 0.682],
      },
      {center: [x1, y1],
        radius: 0.07,
        fillColor: [0.741, 0.741, 0.741],
        strokeColor: [0.682, 0.682, 0.682],
      },
      {center: [x1 - 0.23, y1],
        radius: 0.12,
        fillColor: [1, 1, 1],
        strokeColor: [0.682, 0.682, 0.682],
      },
      {center: [x1, y1 - 0.23],
        radius: 0.12,
        fillColor: [1, 1, 1],
        strokeColor: [0.682, 0.682, 0.682],
      },
      {center: [x1 + 0.23, y1],
        radius: 0.12,
        fillColor: [1, 1, 1],
        strokeColor: [0.682, 0.682, 0.682],
      },
      {center: [x1, y1 + 0.23],
        radius: 0.12,
        fillColor: [1, 1, 1],
        strokeColor: [0.682, 0.682, 0.682],
      },
    ],
  },
  {center: [x2, y2],
    direction: 'clockwise',
    numberOfTeeth: 12,
    radius: 0.3,
    fillColor: [0.741, 0.741, 0.741],
    children: [
      {center: [x2, y2],
        radius: 0.25,
        strokeColor: [0.682, 0.682, 0.682],
      },
      {center: [x2, y2],
        radius: 0.1,
        fillColor: [0.682, 0.682, 0.682],
        strokeColor: [0.6, 0.6, 0.6],
      },
    ],
  },
  {center: [x3, y3],
    direction: 'clockwise',
    numberOfTeeth: 6,
    radius: 0.15,
    fillColor: [0.741, 0.741, 0.741],
    children: [
      {center: [x3, y3],
        radius: 0.1,
        strokeColor: [0.682, 0.682, 0.682],
      },
      {center: [x3, y3],
        radius: 0.02,
        fillColor: [0.682, 0.682, 0.682],
        strokeColor: [0.6, 0.6, 0.6],
      },
    ],
  },
]

对于色彩,有一点须要留神:取值范畴是从 0 到 1,而不是从 0 到 255,或从 0 到 F,这些是咱们在 CSS 中习用的。例如,[0.682,0.682,0.682] 等同于 rgb(174,174,174)#AEAEAE

怎么实现旋转

在开始实现之前须要晓得如何实现每个齿轮的旋转。

为了理解旋转和其余线性变换,我强烈建议你看看 3blue1brown 的线性代数视频课程,该视频很好地阐明了这一点:

(视频 4)

总而言之,如果将地位乘以任何矩阵,都将会失去一个转换。咱们必须将每个齿轮地位乘以旋转矩阵。须要在其后面增加每个“转换”。如果要旋转,咱们将执行 rotation * positions 而不是 positions * rotation

能够通过晓得弧度角来创立旋转矩阵:

function rotation(angleInRadians = 0) {const c = Math.cos(angleInRadians)
  const s = Math.sin(angleInRadians)

  return [
    c, -s, 0, 
    s, c, 0, 
    0, 0, 1
  ]
}

这样就能够通过将每个齿轮的地位与其各自的旋转矩阵相乘来使每个齿轮不同地旋转。为了产生实在的旋转成果,在每个帧中必须略微减少角度,直到实现残缺的旋转,并且角度转回到 0。

然而仅仅将地位与该矩阵相乘是不够的。如果这样做,你将会看到上面这样的后果:

rotationMatrix * positionMatrix // 这不是咱们想要的

咱们曾经使齿轮旋转了,然而旋转轴却是画布的核心,这是谬误的。咱们心愿他们围绕本人的核心旋转。

为了解决这个问题,首先把应用名为 translate 的转换将齿轮挪动到画布的核心。而后,再把利用正确的旋转(该轴将再次成为画布的核心,但在这种状况下,它也是齿轮的核心),最初把齿轮移回其原始地位(再次应用 translate)。

转换矩阵定义如下:

function translation(tx, ty) {
  return [
    1, 0, 0, 
    0, 1, 0, 
    tx, ty, 1
  ]
}

咱们将创立两个转换矩阵:translation(centerX, centerY)translation(-centerX, -centerY)。它们的核心必须是每个齿轮的核心。

所以要执行上面的矩阵乘法:

// 当初它们会围绕本人的轴心旋转
translationMatrix * rotationMatrix * translationToOriginMatrix * positionMatrix

你可能想晓得如何使每个齿轮依照本人的速度旋转。

有一个简略的公式能够依据齿数计算速度:

(Speed A * Number of teeth A) = (Speed B * Number of teeth B)

这样,在每个框架中,咱们能够为每个齿轮减少一个不同的角度步长,并且每个齿轮都以他们应有的速度旋转。

实现

你看到这里应该晓得:

  • 应该画什么,怎么画。
  • 咱们有每个齿轮及其整机的坐标。
  • 怎么旋转每个齿轮。

上面看看如何用 JavaScript 和 GLSL 实现。

用着色器初始化程序

编写 vertex shader 来计算顶点的地位:

const vertexShader = `#version 300 es
precision mediump float;
in vec2 position;
uniform mat3 u_rotation;
uniform mat3 u_translation;
uniform mat3 u_moveOrigin;

void main () {vec2 movedPosition = (u_translation * u_rotation * u_moveOrigin * vec3(position, 1)).xy;
  gl_Position = vec4(movedPosition, 0.0, 1.0);
  gl_PointSize = 1.0;
}
`

与上一篇文章中应用的顶点着色器不同,咱们将传递 u_translationu_rotationu_moveOrigin 矩阵,因而 gl_Position 是四个矩阵的乘积(还有 position)。像上一节所所说的那样,通过这种形式 产生旋转 。另外,咱们将应用 gl_PointSize 定义所绘制的每个点的大小(这对于带有边框的圆很有用)。

留神:咱们能够间接用 JavaScript 在 CPU 上执行矩阵乘法的操作,并且曾经在这里传递了最终矩阵,但实际上 GPU 才是专门为矩阵运算而设计的,因为这样做的性能要好得多。另外因为无奈间接对数组进行乘法运算,所以在 JavaScript 中须要一个辅助函数来进行乘法运算。

上面编写 片段着色器 来计算与每个地位对应的像素色彩:

const fragmentShader = `#version 300 es
precision mediump float;
out vec4 color;
uniform vec3 inputColor;

void main () {color = vec4(inputColor, 1.0);
}
`

给出用 JavaScript 在 CPU 中所定义的色彩,并将其传递给 GPU 来对图形进行着色。

当初能够应用着色器创立程序,通过增加线条来获取咱们在顶点着色器中定义的对立地位。这样稍后在运行脚本时,能够将每个矩阵发送到每一帧的每个对立地位。

const gl = getGLContext(canvas)
const vs = getShader(gl, vertexShader, gl.VERTEX_SHADER)
const fs = getShader(gl, fragmentShader, gl.FRAGMENT_SHADER)
const program = getProgram(gl, vs, fs)
const rotationLocation = gl.getUniformLocation(program, 'u_rotation')
const translationLocation = gl.getUniformLocation(program, 'u_translation')
const moveOriginLocation = gl.getUniformLocation(program, 'u_moveOrigin')

run() // 下一节解释这个函数

getGLContextgetShadergetProgram 实现了咱们在上一篇文章中的操作。我把它们放在这里:

function getGLContext(canvas, bgColor) {const gl = canvas.getContext('webgl2')
  const defaultBgColor = [1, 1, 1, 1]

  gl.clearColor(...(bgColor || defaultBgColor))
  gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT)

  return gl
}

function getShader(gl, shaderSource, shaderType) {const shader = gl.createShader(shaderType)

  gl.shaderSource(shader, shaderSource)
  gl.compileShader(shader)

  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {console.error(gl.getShaderInfoLog(shader))
  }

  return shader
}

function getProgram(gl, vs, fs) {const program = gl.createProgram()

  gl.attachShader(program, vs)
  gl.attachShader(program, fs)
  gl.linkProgram(program)
  gl.useProgram(program)

  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {console.error(gl.getProgramInfoLog(program))
  }

  return program
}

绘制每帧 + 计算旋转角度

上一节代码中的 run 函数负责在每一帧中以不同角度绘制齿轮。

// 1 个齿的齿轮步长,// 齿数更多的步长将用以下公式计算:// realRotationStep = rotationStep / numberOfTeeth
const rotationStep = 0.2

// 角度都初始化为 0
const angles = Array.from({length: gears.length}).map((v) => 0)

function run() {
  // 为每个齿轮计算在该帧的角度
  gears.forEach((gear, index) => {
    const direction = gear.direction === 'clockwise' ? 1 : -1
    const step = direction * (rotationStep / gear.numberOfTeeth)

    angles[index] = (angles[index] + step) % 360
  })

  drawGears() // 下一节解释这个函数

  // Render next frame
  window.requestAnimationFrame(run)
}

依据齿轮组数组中的数据,能够晓得“齿”的数量以及每个齿轮的旋转方向。这样就能够 计算每帧中每个齿轮的角度。保留新的计算角度后调用函数 drawGears 来正确的角度绘制每个齿轮。而后递归地再次调用 run 函数(与window.requestAnimationFrame 包装在一起,确保仅在下一个动画周期中再次调用它)。

你可能想晓得为什么不隐含地通知每一帧之前 革除 canvas。这是因为 WebGL 在绘制时会主动执行。如果它检测到咱们更改了输出变量,则默认状况下会革除之前的缓冲区。如果出于某种原因 不是以后这种状况 咱们不心愿清理画布,那么应该应用附加参数 const gl = canvas.getContext('webgl',{prepareDrawingBuffer: true});

绘制齿轮

对于每帧中的每个齿轮,先把旋转所需的矩阵 u_translationu_rotationu_moveOrigin 传递给 GPU,而后开始绘制齿轮的每个局部:

function drawGears() {gears.forEach((gear, index) => {const [centerX, centerY] = gear.center

    // u_translation
    gl.uniformMatrix3fv(
      translationLocation,
      false,
      translation(centerX, centerY)
    )

    // u_rotation
    gl.uniformMatrix3fv(rotationLocation, false, rotation(angles[index]))

    // u_moveOrigin
    gl.uniformMatrix3fv(
      moveOriginLocation,
      false,
      translation(-centerX, -centerY)
    )

    // 渲染齿轮
    renderGearPiece(gear)
    if (gear.children) gear.children.forEach(renderGearPiece)
  })
}

用雷同的函数绘制齿轮的每个局部:

function renderGearPiece({
  center,
  radius,
  fillColor,
  strokeColor,
  numberOfTeeth,
}) {const { TRIANGLE_STRIP, POINTS, TRIANGLES} = gl
  const coords = getCoords(gl, center, radius)

  if (fillColor) drawShape(coords, fillColor, TRIANGLE_STRIP)
  if (strokeColor) drawShape(coords, strokeColor, POINTS)
  if (numberOfTeeth) {
    drawShape(getCoords(gl, center, radius, numberOfTeeth),
      fillColor,
      TRIANGLES
    )
  }
}
  • 如果是带边界的圆 –> 应用 POINTS
  • 如果是黑白圆 –> 应用 TRIANGLE_STRIP
  • 如果是一个有齿的圆 –> 应用 TRIANGLES

通过应用各种 if,能够创立一个填充有一种色彩但边框是另一种色彩的圆,或者创立一个填充有色彩和齿的圆。这意味着更大的灵活性。

实心圆和带有边界的圆的坐标,即便一个是由三角形组成而另一个是由点制成,也是完全相同的。一个有着不同坐标的带齿的圆,也能够用雷同的代码来获取坐标:

export default function getCoords(gl, center, radiusX, teeth = 0) {
  const toothSize = teeth ? 0.05 : 0
  const step = teeth ? 360 / (teeth * 3) : 1
  const [centerX, centerY] = center
  const positions = []
  const radiusY = (radiusX / gl.canvas.height) * gl.canvas.width

  for (let i = 0; i <= 360; i += step) {
    positions.push(
      centerX,
      centerY,
      centerX + (radiusX + toothSize) * Math.cos(2 * Math.PI * (i / 360)),
      centerY + (radiusY + toothSize) * Math.sin(2 * Math.PI * (i / 360))
    )
  }

  return positions
}

drawShape 的代码与上一篇文章中看到的代码雷同:它将坐标和色彩传递给 GPU,而后调用 drawArrays 函数来批示模式。

function drawShape(coords, color, drawingMode) {const data = new Float32Array(coords)
  const buffer = createAndBindBuffer(gl, gl.ARRAY_BUFFER, gl.STATIC_DRAW, data)

  gl.useProgram(program)
  linkGPUAndCPU(gl, { program, buffer, gpuVariable: 'position'})

  const inputColor = gl.getUniformLocation(program, 'inputColor')
  gl.uniform3fv(inputColor, color)
  gl.drawArrays(drawingMode, 0, coords.length / 2)
}

实现~

所有代码

本文的所有代码在 GitHub 上能够找到,用 Preact 实现的。

  • https://github.com/aralroca/w…

总结

咱们学到如何用三角形和点生成更简单的图形,并实现了基于矩阵乘法的静止。

线(line)是一种咱们尚未见过的绘图模式。那是因为能够用它制作的线很细,并不适宜画齿轮的齿。你不能轻易的更改线条的粗细,而要做到这一点,必须制作一个矩形(2 个三角形)。这些线的灵活性很小,大多数图形都是用三角形绘制的。不过你应该可能轻松应用给定 2 个 坐标的 gl.LINES

本文是 WebGL 系列的第二局部。在本系列的下一篇文章中,咱们将学到纹理、图像处理、帧缓冲区、3d 对象等。


本文首发微信公众号:前端先锋

欢送扫描二维码关注公众号,每天都给你推送陈腐的前端技术文章

欢送持续浏览本专栏其它高赞文章:

  • 深刻了解 Shadow DOM v1
  • 一步步教你用 WebVR 实现虚拟现实游戏
  • 13 个帮你进步开发效率的古代 CSS 框架
  • 疾速上手 BootstrapVue
  • JavaScript 引擎是如何工作的?从调用栈到 Promise 你须要晓得的所有
  • WebSocket 实战:在 Node 和 React 之间进行实时通信
  • 对于 Git 的 20 个面试题
  • 深刻解析 Node.js 的 console.log
  • Node.js 到底是什么?
  • 30 分钟用 Node.js 构建一个 API 服务器
  • Javascript 的对象拷贝
  • 程序员 30 岁前月薪达不到 30K,该何去何从
  • 14 个最好的 JavaScript 数据可视化库
  • 8 个给前端的顶级 VS Code 扩大插件
  • Node.js 多线程齐全指南
  • 把 HTML 转成 PDF 的 4 个计划及实现

  • 更多文章 …

正文完
 0