1. 变量
我们在上一节的基础上增加一些变化:
- 绘制的点跟随鼠标点击位置移动
- 绘制的点双击过后改变颜色
1.1 跟随点击移动
由于我们要更换位置,所以修改的就是顶点着色器代码
// 使用attribute声明变量
// 注意attribute只能用在顶点着色器里面
+ attribute vec4 pos;
void main() {
- gl_Position = vec4(0.5, 0.0, 0.0, 1.0);
+ gl_Position = pos;
gl_PointSize = 30.0;
}
那么,变量的数据怎么来:
// 在主程序中
canvas.current.addEventListener('click', (event) => {
const {
target,
offsetX,
offsetY,
} = event
if(target instanceof HTMLCanvasElement) {
const {
width: eleW,
height: eleH
} = target.getBoundingClientRect()
// 由于webgl的坐标和事件的坐标不一样,所以标准化一下
// 计算原理就是算出相对值并归一化
/**
* html的坐标系原点在左上角,webgl的原点在中心所以苏姚转化
* 1. 转化到canvas坐标系,(offsetX, offsetY)
* 2. 转化到webgl坐标系,(offsetX - halfW, halfH - offsetY)
* 3. 因为webgl是0~1,所以归一化((offsetX - halfW) / halfW, (halfH - offsetY) / halfH)
*/
const halfW = eleW / 2
const halfH = eleH / 2
const clickX = (offsetX - halfW) / halfW
const clickY = (halfH - offsetY) / halfH
// 1. 获取着色程序program中的变量
const pos = gl.getAttribLocation(program, 'pos')
// 2. 设置变量值
gl.vertexAttrib4fv(pos, [clickX, clickY, 0, 1])
// 或者 gl.vertexAttrib4f(pos, clickX, clickY, 0, 1)
// 注意重新渲染时需要clear一次
gl.clearColor(0, 0, 0, 1)
gl.clear(gl.COLOR_BUFFER_BIT)
gl.drawArrays(gl.POINTS, 0, 1)
}
})
1.2 双击换色
变化颜色就是修改片元着色器代码
// 使用uniform定义全局变量,使用变量前需要定义浮点数精度,因为片段着色器里面没有默认精度
// https://stackoverflow.com/questions/28540290/why-it-is-necessary-to-set-precision-for-the-fragment-shader
// 就是没讨论出来是否需要默认, OpenGL ES 2.0 有默认值
+ precision mediump float;
+ uniform vec4 color;
void main() {
- gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0)
+ gl_FragColor = color;
}
uniform
实际上是全局变量,就是在一个渲染程序program
内部的shader
之间都是通用的,也就是说在顶点着色器里面定义uniform
变量,在片段着色器里面也能用。
要注意的是全局变量属于单个着色程序,如果多个着色程序有同名全局变量,需要找到每个全局变量并设置自己的值。 我们调用
gl.uniform???
的时候只是设置了当前程序的全局变量,当前程序是传递给gl.useProgram
的最后一个程序。
主程序中:
canvas.current.addEventListener('dblclick', (e) => {
// 1. 获取uniform变量
const color = gl.getUniformLocation(program, 'color')
// 2. 设置值
gl.uniform4fv(color, [Math.random(), Math.random(), Math.random(), 1])
gl.clearColor(0, 0, 0, 1)
gl.clear(gl.COLOR_BUFFER_BIT)
gl.drawArrays(gl.POINTS, 0, 1)
})
2.缓冲区
有时候我们需要批量生产很多个点,但是使用js
循环重绘不是一个好的方法,特别是图形复杂之后,这个时候我们可以在缓存区里操作。
import { useEffect, useRef } from 'react'
import vert from './index.vert'
import frag from './index.frag'
const WebGL = () => {
const canvas = useRef<HTMLCanvasElement>(null)
useEffect(() => {
if(canvas.current) {
const gl = canvas.current.getContext('webgl')
if(gl) {
gl.clearColor(0, 0, 0, 1)
gl.clear(gl.COLOR_BUFFER_BIT)
const vertexShader = gl.createShader(gl.VERTEX_SHADER)
const fragShader = gl.createShader(gl.FRAGMENT_SHADER)
if(vertexShader && fragShader) {
gl.shaderSource(vertexShader, vert)
gl.shaderSource(fragShader, frag)
gl.compileShader(vertexShader)
gl.compileShader(fragShader)
const program = gl.createProgram()
if(program) {
gl.attachShader(program, vertexShader)
gl.attachShader(program, fragShader)
gl.linkProgram(program)
gl.useProgram(program)
const pos = gl.getAttribLocation(program, 'pos')
// 1. 创建缓冲对象
const buffer = gl.createBuffer()
/**
* 2. 绑定缓冲对象到渲染程序
* gl.ARRAY_BUFFER:表示缓冲区存储的是顶点的数据
* gI.ELEMENT_ARRAY_BUFFER:表示缓冲区存储的是顶点的索引值(就是变量地址)
*/
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
/**
* 3. 给缓冲区赋值
* 第一个参数和bindBuffer一样
* 第三个参数:
* gl.STATIC_DRAW:写入一次,多次绘制
* gl.DYNAMIC_DRAW:写入多次,绘制多次
*/
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
-0.5, 0.5,
0.5, 0.5,
0.5, -0.5,
-0.5, -0.5,
]),
gl.STATIC_DRAW
)
/**
* 4. 告诉显卡读取数据并设定一些布局
* 参数1 index:变量地址(顶点属性的索引)
* 参数2 size:绘制所需要的数据维度
* 参数3 type:数据格式
* gl.FLOAT:浮点型
* gl.UNSIGNED_BYTE:无符号字节
* gI.SHORT:短整型
* gl.UNSIGNED_SHORT:无符号整型
* gl.INT:整型
* gl.UNSIGNED_INT:无符号整型
* webgl2里面有gl.HALF_FLOAT
* 参数4 normalized:数据是否归一化,gl.FLOAT无效
* 参数5 stride:每一行之间的偏移量,两个顶点之间的
* 参数6 offset:数据的偏移量
*/
gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0)
/**
* 5. 激活属性变量
*/
gl.enableVertexAttribArray(pos)
gl.drawArrays(gl.POINTS, 0, 4)
}
}
}
}
}, [])
return (
<div style = { {padding: 20, border: 'solid 2px #000', margin: 20, display: 'inline-block'} }>
<canvas ref = { canvas } width = { 600 } height = { 600 }></canvas>
</div>
)
}
export default WebGL
// 顶点着色器
attribute vec4 pos;
void main() {
gl_Position = pos;
gl_PointSize = 30.0;
}
// 片段着色器
precision mediump float;
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
效果
3.多个缓冲区
简单的来说,就是创建多个缓冲区分别激活,现在我们把四个点分别设置大小为10、20、30、40
// .....
const size = gl.getAttribLocation(program, 'size')
const sizeBuffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, sizeBuffer)
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
10,
20,
30,
40
]),
gl.STATIC_DRAW)
// 注意这里的维度参数,不是二维了
gl.vertexAttribPointer(size, 1, gl.FLOAT, false, 0, 0)
gl.enableVertexAttribArray(size)
// 在drawArrays之前增加一个缓冲对象
gl.drawArrays(gl.POINTS, 0, 4)
顶点着色器
attribute vec4 pos;
+ attribute float size;
void main() {
gl_Position = pos;
- gl_PointSize = 30.0;
+ gl_PointSize = size;
}
效果
虽然满足了效果但是很傻,要是我要改变更多的变量不得重新写好多遍。还记得vertexAttribPointer
这个函数的参数吗?最后两个参数表示的是行偏移量、列偏移量。那我们可不可以把上面代码中的位置、大小放入一个数组,然后设置不同偏移量装入缓冲区。
实现
import { useEffect, useRef } from 'react'
import vert from './index.vert'
import frag from './index.frag'
const WebGL = () => {
const canvas = useRef<HTMLCanvasElement>(null)
useEffect(() => {
if(canvas.current) {
const gl = canvas.current.getContext('webgl')
if(gl) {
gl.clearColor(0, 0, 0, 1)
gl.clear(gl.COLOR_BUFFER_BIT)
const vertexShader = gl.createShader(gl.VERTEX_SHADER)
const fragShader = gl.createShader(gl.FRAGMENT_SHADER)
if(vertexShader && fragShader) {
gl.shaderSource(vertexShader, vert)
gl.shaderSource(fragShader, frag)
gl.compileShader(vertexShader)
gl.compileShader(fragShader)
const program = gl.createProgram()
if(program) {
gl.attachShader(program, vertexShader)
gl.attachShader(program, fragShader)
gl.linkProgram(program)
gl.useProgram(program)
const pos = gl.getAttribLocation(program, 'pos')
const size = gl.getAttribLocation(program, 'size')
// 1. 创建缓冲对象
const buffer = gl.createBuffer()
/**
* 2. 绑定缓冲对象到渲染程序
*/
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
/**
* 3. 给缓冲区赋值
* 这里多了一个维度,添加了大小
*/
const data = new Float32Array([
-0.5, 0.5, 10,
0.5, 0.5, 20,
0.5, -0.5, 30,
-0.5, -0.5, 40
])
gl.bufferData(
gl.ARRAY_BUFFER,
data,
gl.STATIC_DRAW
)
// 因为偏移量是按照字节为单位,所以获取每个元素的字节大小
const dataSize = data.BYTES_PER_ELEMENT
/**
* 4. 告诉显卡读取数据并设定一些布局
* 为什么stride设置为dataSize * 3?
* 因为数据维度size取的2,如果stride设置为0,就会挨着两个两个的取
*/
gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, dataSize * 3, 0)
/**
* offset的值为dataSize * 2,因为size的值实际取的是index为2的值
*/
gl.vertexAttribPointer(size, 1, gl.FLOAT, false, dataSize * 3, dataSize * 2)
/**
* 5. 激活属性变量
*/
gl.enableVertexAttribArray(pos)
gl.enableVertexAttribArray(size)
gl.drawArrays(gl.POINTS, 0, 4)
}
}
}
}
}, [])
return (
<div style = { {padding: 20, border: 'solid 2px #000', margin: 20, display: 'inline-block'} }>
<canvas ref = { canvas } width = { 600 } height = { 600 }></canvas>
</div>
)
}
export default WebGL
4. 其他图形
4.1 线段
我们接着用上面的代码
// 只需要改这一步
- gl.drawArrays(gl.POINTS, 0, 4)
+ gl.drawArrays(gl.LINES, 0, 4)
drawArrays
第一个参数就是绘制方式
效果
可以发现是两两连接成了一条线,为什么是这样的呢?因为
gl.LINES
代表的就是绘制一系列单独线段。每两个点作为端点,线段之间不连接。
所以改成LINE_STRIP
可以发现连起来了
- gl.drawArrays(gl.LINES, 0, 4)
+ gl.drawArrays(gl.LINE_STRIP, 0, 4)
效果
其他的情况可以自己试试
4.2 三角形
同样的只需要改变参数
- gl.drawArrays(gl.LINES, 0, 4)
+ gl.drawArrays(gl.TRIANGLES, 0, 4)
这里需要注意,三角形只需要3个顶点,但是我这里给了4个,没关系,程序会截取3的倍数作为顶点
我换个参数试试
- gl.drawArrays(gl.TRIANGLES, 0, 4)
// 三角扇
+ gl.drawArrays(gl.TRIANGLE_FAN, 0, 4)
使用三角扇的效果,字母是我加的
为什么看着像正方形,因为三角扇的定义。这四个点画出来的三角形共享了同一个顶点也就是
A
,所以ABC
、ACD
两个三角形组成了现在这个样子。
再换个参数试试
- gl.drawArrays(gl.TRIANGLE_FAN, 0, 4)
// 三角带
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
效果,标注是我额外加的:
这里绘制了两个三角形
123
和234
,为什么选择这几个点。这里有个公式
4.2.1 TRIANGLE_STRIP
理解
4.2.2 TRIANGLE_STRIP
和 TRIANGLE_FAN
的区别
- 他们都是为了减少计算机运算量,而产生的的各种生成连续三角形的方法
- Triangle Strip中的三角形是相邻的三角形,而Triangle Fan的三角形指的是以一个中心点形成的平面上的连续三角形。
- 两者的主要区别在于Triangle Strip中的三角形是相邻的,而Triangle Fan中的三角形则不一定是连续的。