接前面文章,我们实现了一个最简单的 webgl 程序,但是只是程序运行起来只是漆黑一片,我们想要通过 webgl 绘制一些图形。这篇文章让我们来使用 webgl 绘制一个点,并使用着色器来给他点颜色看看。
还是先上代码:
import React, { useRef, useEffect } from 'react'
import { VSHADERR_SOURCE, FSHADER_SOURCE } from './glsl'
const HelloPoint = () => {
const canvasDom = useRef<HTMLCanvasElement | null>(null)
// 创建程序
const createProgram = (gl: WebGLRenderingContext, vshader: string, fshader: string) => {
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader)
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader)
if (!vertexShader || !fragmentShader) {
return null
}
// 创建程序对象
const program = gl.createProgram()
if (!program) {
return null
}
// 为程序对象分配着色器
gl.attachShader(program, vertexShader)
gl.attachShader(program, fragmentShader)
// 连接程序对象
gl.linkProgram(program)
// Check the result of linking
const linked = gl.getProgramParameter(program, gl.LINK_STATUS)
if (!linked) {
const error = gl.getProgramInfoLog(program)
console.log(`Failed to link program: ${error}`)
gl.deleteProgram(program)
gl.deleteShader(fragmentShader)
gl.deleteShader(vertexShader)
return null
}
return program
}
const loadShader = (gl: WebGLRenderingContext, type: number, source: string) => {
// 创建 shader 对象
const shader = gl.createShader(type)
if (shader === null) {
console.log('unable to create shader')
return null
}
// 向着色器对象中填充着色器程序的源代码
gl.shaderSource(shader, source)
// 编译着色器
gl.compileShader(shader)
// Check the result of compilation
const compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS)
if(!compiled) {
const error = gl.getShaderInfoLog(shader)
console.log(`Failed to compile shader: ${error}`)
gl.deleteShader(shader)
return null
}
return shader
}
// 初始化着色器
const initShader = (gl: WebGLRenderingContext, vshader: string, fshader: string) => {
const program = createProgram(gl, vshader, fshader)
if (!program) {
console.log('Failed to create program')
return false
}
// 使用程序对象
gl.useProgram(program);
(gl as any).program = program
return true
}
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
}
// 初始化着色器
if (!initShader(gl, VSHADERR_SOURCE, FSHADER_SOURCE)) {
console.log('Failed to intialize shaders.')
return
}
// 获取 attribute 变量的存储位置
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
}
// 将顶点位置传输给 attribute 变量
gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0)
// 指定清空 canavs 的颜色
gl.clearColor(0.0, 0.0, 0.0, 1.0)
// 清空 canvas
gl.clear(gl.COLOR_BUFFER_BIT)
// 绘制一个点
gl.drawArrays(gl.POINTS, 0, 1)
}
useEffect(() => {
main()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return (
<canvas ref={canvasDom} style={{
width: '500px',
height: '500px'
}}></canvas>
)
}
export default HelloPoint
代码有点多,我们慢慢来看,首先看初始化着色器部分;
在三维场景中,仅仅用线条和颜色将图层绘制出来是远远不够的,比如我们需要模拟光照打在物体上,高光部门以及阴影部分的展示等等;着色器可以高度灵活地完成对于图形的这些各种渲染效果。
创建和初始化着色器需要以下几个步骤:
步骤也很简单也就是创建着色器,然后通过 program 将顶点着色器和片元着色器连接起来;program 对象进行着色器连接操作,目的是保证:
如果程序已经成功连接,我们得到了一个二进制的可执行模块供 webgl 系统使用。如果连接失败了,也可以通过调用 gl.getProgramInfiLog(program) 从信息日志中获取连接出错信息。
最后我们通过 gl.useProgram(program) 告知 webgl 系统绘制时使用哪个程序对象,该函数接收一个指定待使用的程序对象做为参数。
我们可以在绘制前初始化多个程序对象,然后在绘制的时候根据需要切换程序对象。
我们发现;在 JavaScript 中我们通过 gl.getAttribLocation() 函数获取 webgl 程序变量的存储地址。到现在我们也没看到着色器代码;如下:
export const VSHADERR_SOURCE = `
attribute vec4 a_Position;
void main() {
// 设置坐标
gl_Position = a_Position;
// 设置尺寸
gl_PointSize = 10.0;
}
`
export const FSHADER_SOURCE = `
void main() {
// 设置点的颜色
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`
这是 GLSL 着色器语言,很简单相信大家可以看懂。主要需要注意的是,attribute 数据类型。对于着色器语言,具有三种数据类型:
我们上面代码通过 attribute 类型的变量传递我们绘制点的坐标。
接下来就是需要向 webgl 系统中传递绘制点的坐标;传递值我们首先得获取该值的位置,如下代码:
// 获取 attribute 变量的存储位置
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
}
对于 attribute 定义的变量通过 getAttribLocation() 方法进行获取,当然通过 uniform 定义的变量通过 getUniformLocation() 方法进行获取。获取存储位置其实是从 0 开始根据定义的位置依次计数的结果;
然后获取到位置,我们需要将值传输给变量:
// 将顶点位置传输给 attribute 变量
gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0)
需要注意的是,webgl 使用的是颜色缓冲区,webgl 系统中的绘制操作实际上是在颜色缓冲区中进行绘制的,绘制结束后系统将缓冲区中的内容显示在屏幕上,然后颜色缓冲区就会被重置,其中的内容会丢失;如果我们需要实时绘制点,比如鼠标点击之后绘制点,需要将鼠标点击的点进行记录,每次点击完成之后通过循环,渲染之前的点。
至此,我们的着色器使用已经完成。