webgl——图形的基本变换:平移、旋转、缩放并实践类似 threeJS 放大、拖拽效果(五)


前言

接上篇,我们实现了一个可旋转的彩色立方体。我们并没有详细说明里面的一个模型矩阵变换;本篇文章将解决如何对一个图形进行平移、旋转、缩放;并实战一个似 threeJS 的可以利用鼠标滚轮以及鼠标事件来对图形操作。


一、移动、旋转、缩放

首先需要将世界坐标(我们以自己的视角定义的物体的坐标,如前面例子绘制一个 “F”)转换为 webgl 中的坐标 [-1, 1] 的范围内,这会用到缩放、然后在 webgl 中如何放置,放在哪里;我们需要用到平移、旋转等。这样的操作称为变换或仿射变换。

1.平移

首先我们可以想象一下平移的操作,无非就是对一个物体的 x、y 坐标进行添加一定的偏移量来实现:

在这里插入图片描述
如上图,我们将 p (x, y, z) 点移动到 p’(x’, y’, z’);对于 p’ 的坐标我们可以通过下面表达式计算:

x' = x + Tx
y' = y + Ty

我们只需要着色器中为顶点坐标的每个分量加上一个常量就可以实现上面的等式。显然,这是一个逐顶点操作而非逐片元操作,上面的操作发生在顶点着色器,而不是片元着色器。

拿我们之前绘制的 “F” 代码为例(文章)我们修改着色器部分代码:

export const VSHADERR_SOURCE = `
  attribute vec2 a_Position;
  uniform vec2 u_resolution;
  void main() {
    vec2 translate = vec2(a_Position.x + 18.0, a_Position.y + 15.0);
    //[0, 1]
    vec2 zeroToOne = translate / u_resolution;
    //[0, 2]
    vec2 zeroToTwo = zeroToOne * 2.0;
    //[-1, 1]
    vec2 clipSpace = zeroToTwo - 1.0;
    gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
  }
`

export const FSHADER_SOURCE = `
  precision mediump float;
  uniform vec4 u_color;
  void main() {
    gl_FragColor = u_color;
  }
`

主要看:vec2 translate = vec2(a_Position.x + 18.0, a_Position.y + 15.0);
将它的 x,y 坐标分别加了一个偏移量(注意声明一个 vec2 类型,通过vec2() 方法进行定义);我们会看到 F 图形进行了平移。
学过矩阵的童鞋都知道,上面的表达式可以通过一个变换矩阵相乘来解决;变换矩阵非常适合操作计算机图形。我们的坐标可以看做一个 1 × 3 的一个矩阵;将上面的表达式写成矩阵相乘如下:

x' = x + Tx
y' = y + Ty

1 0 Tx            x                      x + Tx 
0 1 Ty     ×      y            =         y + Ty
0 0 1             1                        1

这里矩阵上升到了三维矩阵,因为二维矩阵无法实现我们要实现的表达式;这种增加一个维度的做法,我们叫做齐次坐标。
对于矩阵的相乘,这里就不做说明,可以看我这里的数学基础这篇文章。
这里只是变换了 x,y 坐标进行平移,如果进行 x、y、z 进行平移;那么我们需要一个 4 * 4 的矩阵进行相乘。

我们现在要做的是封装一个方法然后去生成一个平移的矩阵来实现,代码如下:

// webgl 会对矩阵进行一次转置
export const translate2d = (x: number, y: number) => {
  return [
    1, 0, 0,
    0, 1, 0,
    x, y, 1
  ]
}

我们修改之前的着色器代码,如下:

export const VSHADERR_SOURCE = `
  attribute vec2 a_Position;
  // 世界坐标
  uniform mat3 u_world;
  uniform mat3 u_unit;

  void main() {
    vec3 p = u_world * u_unit * vec3(a_Position, 1);
    gl_Position = vec4(p.xy, 0, 1);
  }
`

这里有两个变量 u_world 跟 u_unit;u_world 是世界坐标的转换矩阵;我们在写 “F” 例子的时候说过通过归一化将坐标转换到 webgl 坐标系统内,如下图:
在这里插入图片描述
所以我们需要先缩放、然后平移操作;
在这里插入图片描述
该矩阵如下:

    // 将我们的世界坐标转换到 webgl 坐标系统
    // 同样注意 y 轴;我们世界坐标是 [0, height];在 webgl 绘制中从上往下。
    const worldMatrix = [
      2/gl.canvas.width, 0, 0,
      0, -2/gl.canvas.height, 0,
      -1, +1, 1
    ]

然后我们将值传递到着色器,代码如下:

import React, { useRef, useEffect } from 'react'
import { VSHADERR_SOURCE, FSHADER_SOURCE } from './glsl'
import { initShader } from '../../utils/webglFunc'
import { d2_f as shape } from '../DrawF/df2'
import { translate2d } from '../../utils/translate'
import './index.css'

const ThreeJS = () => {
  const canvasDom = useRef<HTMLCanvasElement | null>(null)

  const initVertexBuffers = (gl: WebGLRenderingContext) => {
    // 定义顶点
    const verticies = shape(100, 100, 100, 150, 30)
    // 创建缓冲区对象
    const vertexBuffer = gl.createBuffer()
    // 将缓冲区对象绑定到目标
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
    // 向缓冲区对象中写入数据,也就是描述buffer 被绑定在哪里。 gl.ARRAY_BUFFER 存顶点属性,gl.ELEMENT_ARRAY_BUFFER 存储顶点索引的类型
    // 最后一个参数是提示 webgl 数据将如何被使用 gl.STATIC_DRAW 表示数据通常不会发生变化,表示 gl.DYNAMIC_DRAW 会发生变化的数据
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verticies), gl.STATIC_DRAW)
    // 在顶点着色器中获取 a_Position 的地址
    // gl.program 为包含顶点着色器和片元着色器的着色器程序对象
    const a_Position = gl.getAttribLocation((gl as any).program, 'a_Position')
    // 获取存储位置其实是从 0 开始根据定义的位置依次计数的结果
    if (a_Position < 0) {
      console.log('Failed to get the storage location of a_Position')
      return
    }
    // 将缓冲区对象分配给 a_Position 变量
    gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0)
    // 连接 a_Position 变量与分配给他的缓冲区对象
    gl.enableVertexAttribArray(a_Position)
    return verticies.length
  }

  const main = () => {
    if (!canvasDom.current) return
    const gl = canvasDom.current.getContext('webgl')

    if (!gl) {
      console.log('Failed to get the rendering context for WebGL')
      return
    }
    gl.canvas.width = (gl as any).canvas.clientWidth
    gl.canvas.height = (gl as any).canvas.clientHeight
    
    // 初始化着色器
    if (!initShader(gl, VSHADERR_SOURCE, FSHADER_SOURCE)) {
      console.log('Failed to intialize shaders.')
      return
    }

    const n = initVertexBuffers(gl)
    const u_Color = gl.getUniformLocation((gl as any).program, 'u_color')

    const u_unit = gl.getUniformLocation((gl as any).program, 'u_unit')
    const u_world = gl.getUniformLocation((gl as any).program, 'u_world')
    gl.uniform4fv(u_Color, [Math.random(), Math.random(), Math.random(), 1.0])

    // 将我们的世界坐标转换到 webgl 坐标系统
    // 同样注意 y 轴;我们世界坐标是 [0, height];在 webgl 绘制中从上往下。
    const worldMatrix = [
      2/gl.canvas.width, 0, 0,
      0, -2/gl.canvas.height, 0,
      -1, +1, 1
    ]

    const matTranslate = translate2d(300.0, 12.0)
    gl.uniformMatrix3fv(u_unit, false, matTranslate)
    gl.uniformMatrix3fv(u_world, false, worldMatrix)

    // 指定清空 canavs 的颜色
    gl.clearColor(0.0, 0.0, 0.0, 1.0)

    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height)
    // 清空 canvas
    gl.clear(gl.COLOR_BUFFER_BIT)

    // 第一个参数为绘制的类型,第二个参数是指定从哪个顶点开始绘制(整数型,从第一个点开始就是 0),
    // 第三个参数是指定绘制需要用到多少个顶点(整数型)
    // 当程序调用 l.drawArrays 方法时,顶点着色器将被执行 n 次,每次处理一个顶点。执行顶点着色器代码中的 main 函数。
    // 一旦顶点着色器执行完之后,片元着色器开始执行。
    if (!n) return
    gl.drawArrays(gl.TRIANGLES, 0, n / 2)
  }

  useEffect(() => {
    main()
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])

  return (
    <canvas ref={canvasDom}></canvas>
  )
}

export default ThreeJS

2. 旋转

我们先说简单的旋转,相对于原点进行旋转,如下图:
在这里插入图片描述
相对于原点旋转的功能可以看做一个生成矩阵的函数 R(θ):
在这里插入图片描述
封装对应函数如下:

/**
 * 相对于原点旋转
 * @param a 旋转角度(弧度制)
 * @returns 
 */
export const rotate2d = (a: number) => {
  return [
    Math.cos(a), Math.sin(a), 0,
    -Math.sin(a), Math.cos(a), 0,
    0, 0, 1
  ]
}

如果想实现根据任意一点进行旋转,可以先将图形移动到原点,然后进行旋转,然后又移动到之前的位置。这样一个复合的操作,来实现任意一点的旋转。

提到复合操作也就是对矩阵进行相乘,实现任意一点旋转就是:

平移矩阵 * 旋转矩阵 * 平移矩阵

这样的操作;我们需要封装一个矩阵相乘的方法,代码如下:

import { identity3d } from './identity'
export const multiply = (a: number[], b: number[], m: number, p: number) => {
  const n = a.length / m
  const q = b.length / p

  if (n !== p) {
    // eslint-disable-next-line no-throw-literal
    throw 'cannot apply multiplication:matrix shape not match'
  }
  const r = [m * q]
  for (let j = 0; j < q; j ++) {
    for (let i = 0; i < m; i++) {
      let s = 0
      for (let k = 0; k < n; k++) {
        s += a[k + i * n] * b[j + k * q]
      }
      r[i * q + j] = s
    }
  }
  return r
}


export const multiply3d = (...matriexs: number[][]) => {
  return matriexs.reduce((a, b) => multiply(a, b, 3, 3), identity3d())
}

然后我们执行矩阵相乘得到旋转矩阵,代码如下:

  const main = () => {
    // 将我们的世界坐标转换到 webgl 坐标系统
    // 同样注意 y 轴;我们世界坐标是 [0, height];在 webgl 绘制中从上往下。
    const worldMatrix = [
      2/gl.canvas.width, 0, 0,
      0, -2/gl.canvas.height, 0,
      -1, +1, 1
    ]

    let uMatrix = identity3d()
    const matTranslate = translate2d(300.0, 12.0)
    // 相对于原点旋转
    const matRotate = rotate2d(20)

    // 任意一点的旋转,首先先移动到原点,然后进行旋转,然后移回去之前的位置
    const matBeforeRotate = translate2d(-50, -75)
    const matAfterRotate = translate2d(50, 75)
    uMatrix = multiply3d(uMatrix, matBeforeRotate, matRotate, matAfterRotate)
    gl.uniformMatrix3fv(u_unit, false, uMatrix )
    gl.uniformMatrix3fv(u_world, false, worldMatrix)
  }

3. 缩放

缩放就很简单了,直接对 x,y 坐标进行乘以不同的缩放比例即可,矩阵形式如下:

在这里插入图片描述
封装生成的对应代码:

export const scale2d = (sx: number, sy: number) => {
  return [
    sx, 0, 0,
    0, sy, 0,
    0, 0, 1
  ]
}

二、封装鼠标相关事件

代码如下(示例):

import React, { useRef, useEffect } from 'react'
import { VSHADERR_SOURCE, FSHADER_SOURCE } from './glsl'
import { initShader } from '../../utils/webglFunc'
import { d2_f as shape } from '../DrawF/df2'
import { multiply3d } from '../../utils/multiply'
import { identity3d } from '../../utils/identity'
import { translate2d } from '../../utils/translate'
import { scale2d } from '../../utils/scale'
import { rotate2d } from '../../utils/rotate'
import './index.css'

const ThreeJS = () => {
  const canvasDom = useRef<HTMLCanvasElement | null>(null)
  const glContext = useRef<WebGLRenderingContext | null>()
  const drawNum = useRef<number>()

  const initVertexBuffers = (gl: WebGLRenderingContext) => {
    // 定义顶点
    const verticies = shape(100, 100, 100, 150, 30)
    // 创建缓冲区对象
    const vertexBuffer = gl.createBuffer()
    // 将缓冲区对象绑定到目标
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
    // 向缓冲区对象中写入数据,也就是描述buffer 被绑定在哪里。 gl.ARRAY_BUFFER 存顶点属性,gl.ELEMENT_ARRAY_BUFFER 存储顶点索引的类型
    // 最后一个参数是提示 webgl 数据将如何被使用 gl.STATIC_DRAW 表示数据通常不会发生变化,表示 gl.DYNAMIC_DRAW 会发生变化的数据
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verticies), gl.STATIC_DRAW)
    // 在顶点着色器中获取 a_Position 的地址
    // gl.program 为包含顶点着色器和片元着色器的着色器程序对象
    const a_Position = gl.getAttribLocation((gl as any).program, 'a_Position')
    // 获取存储位置其实是从 0 开始根据定义的位置依次计数的结果
    if (a_Position < 0) {
      console.log('Failed to get the storage location of a_Position')
      return
    }
    // 将缓冲区对象分配给 a_Position 变量
    gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0)
    // 连接 a_Position 变量与分配给他的缓冲区对象
    gl.enableVertexAttribArray(a_Position)
    return verticies.length
  }

  const main = () => {
    if (!canvasDom.current) return
    const gl = canvasDom.current.getContext('webgl')
    glContext.current = gl
    if (!gl) {
      console.log('Failed to get the rendering context for WebGL')
      return
    }
    gl.canvas.width = (gl as any).canvas.clientWidth
    gl.canvas.height = (gl as any).canvas.clientHeight
    
    // 初始化着色器
    if (!initShader(gl, VSHADERR_SOURCE, FSHADER_SOURCE)) {
      console.log('Failed to intialize shaders.')
      return
    }

    const n = initVertexBuffers(gl)
    const u_Color = gl.getUniformLocation((gl as any).program, 'u_color')

    const u_unit = gl.getUniformLocation((gl as any).program, 'u_unit')
    const u_world = gl.getUniformLocation((gl as any).program, 'u_world')
    gl.uniform4fv(u_Color, [Math.random(), Math.random(), Math.random(), 1.0])

    // 将我们的世界坐标转换到 webgl 坐标系统
    // 同样注意 y 轴;我们世界坐标是 [0, height];在 webgl 绘制中从上往下。
    const worldMatrix = [
      2/gl.canvas.width, 0, 0,
      0, -2/gl.canvas.height, 0,
      -1, +1, 1
    ]

    let uMatrix = identity3d()
    // const matTranslate = translate2d(300.0, 12.0)
    // 相对于原点旋转
    // const matRotate = rotate2d(20)

    uMatrix = multiply3d(uMatrix)
    gl.uniformMatrix3fv(u_unit, false, uMatrix)
    gl.uniformMatrix3fv(u_world, false, worldMatrix)

    // 指定清空 canavs 的颜色
    gl.clearColor(0.0, 0.0, 0.0, 1.0)

    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height)
    // 清空 canvas
    gl.clear(gl.COLOR_BUFFER_BIT)
    if (!n || !u_unit) return
    drawNum.current = n
    draw(gl, n, u_unit, uMatrix)
  }

  const draw = (gl: WebGLRenderingContext, n: number, u_unit: WebGLUniformLocation, uMatrix: number[]) => {
    // 清空 canvas
    gl.clear(gl.COLOR_BUFFER_BIT)

    gl.uniformMatrix3fv(u_unit, false, uMatrix)
    // gl.uniformMatrix3fv(u_world, false, worldMatrix)
    gl.drawArrays(gl.TRIANGLES, 0, n / 2)
  }

  let scale = 1
  const handleScroll = (event: any) => {
    if (!glContext.current || !drawNum.current) return
    // 拉伸
    const matScale = scale2d(scale, scale)
    let uMatrix = identity3d()
    uMatrix = multiply3d(uMatrix, matScale)
    const u_unit = glContext.current.getUniformLocation((glContext.current as any).program, 'u_unit')
    if (!u_unit) return
    //判断鼠标滚轮的上下滑动
    let deta = event.deltaY
    if(deta > 0){
      scale += 1
      draw(glContext.current, drawNum.current, u_unit, uMatrix)
    }
    if(deta < 0){
      scale -= 0.5
      if (scale < 0) return
      draw(glContext.current, drawNum.current, u_unit, uMatrix)
    }
  }

  let timmerHandle: any = null
  let isDrag = false
  let drag = {
    x: 0,
    y: 0
  }
  const down = (event: any) => {
    console.log("mouse down.", event, drag)
    drag.x = event.clientX
    drag.y = event.clientY
    isDrag = false
    // 延迟100ms
    timmerHandle = setTimeout(setDragTrue,200)
  }
  const setDragTrue = () => {
    isDrag = true
  }

  const up = (event: any) => {
    if (!isDrag) {
      //先把doMouseDownTimmer清除,不然200毫秒后setGragTrue方法还是会被调用的
      clearTimeout(timmerHandle) 
    } else {
      isDrag = false
      if (!glContext.current || !drawNum.current) return
      const matTranslate = translate2d(event.clientX - drag.x, event.clientY - drag.y)
      let uMatrix = identity3d()
      uMatrix = multiply3d(uMatrix, matTranslate)
      const u_unit = glContext.current.getUniformLocation((glContext.current as any).program, 'u_unit')
      if (!u_unit) return
      draw(glContext.current, drawNum.current, u_unit, uMatrix)
    }
  }

  useEffect(() => {
    main()
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])

  return (
    <canvas ref={canvasDom} onWheel={handleScroll} onMouseDown={down} onMouseUp={up}></canvas>
  )
}

export default ThreeJS

类似的功能,还是存在一些小bug,后续完善。

总结

至此,对图形的缩放基本讲完;下面我们就需要说一下投影相关的问题。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jiegiser#

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值