How GPU works
图形管道
任何3D图形系统的目的是根据场景描述合成图像;GPU设计者通常将这种图像合成过程表示为一个专用阶段的硬件管道。在这里,我们提供了一个经典图形管道的高级概述——我们的目标是强调实时渲染计算的那些方面——让图形应用程序开发人员利用现代gpu作为通用并行计算引擎。
管道输入
大多数实时图形处理系统会把所有事物看做是由许多个三角形组成的。因此图形处理系统首先会把复杂的输入图像分割为许多三角形。然后开发者每次提供每个三角形的一个顶点给图像管道,gpu再根据需要将这些顶点组装成三角形
模型转换
GPU能够在它局部定义的坐标系确定场景中的每个逻辑对象。这对于多层级的对象场景来说是更加方便,这个使用自定义的坐标系统进行坐标转换有时候能极大降低计算的复杂度。
当然,代价就是绘制前对所有顶点通过平移、旋转、缩放等进行坐标转换。这些变换可以使用矩阵向量的乘法表示。每秒钟百万级的单精度浮点数向量运算也驱动了gpu并行计算的变革。
模型转换阶段管道的输出是三维坐标系中表示的三角形流。
光照
当每个三角形位于全局坐标系中,gpu可以根据场景中的光照计算它的颜色
相机模拟
接下来,图形管道将每个彩色3D三角形投影到虚拟相机的胶片平面上。像模型转换一样,GPU使用矩阵向量乘法来实现这一点,同样在硬件中利用高效的向量操作。这个阶段的输出是一个以屏幕坐标表示的三角形流,准备好转换为像素
光栅化
光栅化(Rasterization)是把顶点数据转换为片元的过程,具有将图转化为一个个栅格组成的图像的作用,特点是每个元素对应帧缓冲区中的一像素。
光栅化其实是一种将几何图元变为二维图像的过程。该过程包含了两部分的工作。第一部分工作:决定窗口坐标中的哪些整型栅格区域被基本图元占用;第二部分工作:分配一个颜色值和一个深度值到各个区域。光栅化过程产生的是片元。
光栅化(Rasterisation)是一项任务——将一幅矢量图形格式(图形)描述的图像转换为光栅图像(一系列像素、点或线,当它们一起显示时,创建通过图形表示的图像)。
把物体的数学描述以及与物体相关的颜色信息转换为屏幕上用于对应位置的像素及用于填充像素的颜色,这个过程称为光栅化,这是一个将模拟信号转化为离散信号的过程。
纹理化
每个像素的实际颜色可以直接从光照计算中获得,但是为了增加真实性,称为纹理的图像通常覆盖在几何体上,以产生细节的错觉。GPU将这些纹理存储在高速存储器中,每个像素计算都必须访问该内存以决定或者修改像素颜色。
在实践中,GPU可能需要对每个像素进行多次纹理访问以减轻由于纹理显示在比原生分辨率更大或者更小的屏幕上时造成的视觉伪影。因为纹理内存的访问模式通常是非常有规律的(附近的像素往往访问附近的纹理图像位置),专门的缓存设计有助于隐藏内存访问的延迟
隐藏面
多个几何体出现遮挡时,需要做消隐
处理(消除隐藏线或隐藏面),否则最近处理的三角形可能位于最上层,导致出现与期望不一样的结果;
因此,正确的隐藏表面移除需要对每个视图的所有三角形进行从后到前的排序,这是一个昂贵的操作,甚至不是所有场景都能做到。
所有现代gpu都提供一个深度缓冲区,一个存储每个像素到观察者的距离的内存区域。在写入显示器之前,GPU将一个像素的距离与已经存在的像素的距离进行比较,只有当新像素更接近时才会更新显示内存
基础
- GLSL语言:即OpenGL Shading Language,大小写敏感
Shading
概述:用于为每个可编程处理器创建shader
,这些处理器包括vertex
、fragment
、tessellation control
、tessellation evaluation
等;tessellation evaluation
:WebGL
只在乎两件事:裁剪空间坐标和颜色,作为webgl开发者的你需要向WebGL提供这两个信息,你提供的顶点着色器和片元着色器的正是用于此的;
不论画布大小,裁剪空间坐标的范围都是-1到1的;- GLSL中合法的合法字符:
字母:a-z、A-Z、_
数字:0-9
符号:.
、+
、-
、*
、/
、%
、<
、[
、(
、{
、^
、|
、&
、~
、=
、!
、:
、;
、?
出现其他字符的话,会导致GLSL编译错误; - 变量和类型:
ype | 释义 |
void | |
bool | |
int | |
float | |
double | |
vec | float类型向量 |
ivec | int类型向量 |
bvec | bool类型向量 |
mat2 | 2*2的单精度矩阵 |
mat2x3 | 2*3的单精度矩阵 |
… | … |
- 限定符
存储限定符 | 含义 |
<none: default> | 本地读写内存,或函数入参 |
const | |
in | 从上一阶段连接到着色器,变量被拷贝到当前阶段 |
out | 链接着色器到下一个阶段,变量被拷贝到下一阶段 |
attribute | 仅用作兼容性配置中或顶点语言中,在顶点着色器中等同 |
uniform | 基元处理过程中值不会改变, |
varying | 仅用作兼容性配置、vertex和fragment着色器;在 |
buffer | 值被存储在一个buffer对象中,能够被着色器和API读写 |
shared | 仅用于计算着色器;变量存储在工作组中的所有工作项间共享 |
辅助限定符仅可在in
和out
存储限定符中使用
局部变量仅可以使用const
存储限定符或者不使用任何存储限定符
辅助存储限定符 | 含义 |
centroid | 基于质心的插值 |
sample | 各样本插值 |
patch | per-tessellation-patch attributes |
- 坐标系
WebGL
是一个三维直角坐标系。它的坐标原点比较特殊,是画布的中心位置;x轴
向右,y轴
向下,z轴
朝外,并且x轴
和y轴
在画布中的范围是-1至1; - GLSL着色器
GLSL是可以在图形管道(graphic pipeline)中直接执行的OpenGL着色语言;着色器类型包括:顶点着色器(vertex shader)
和片元着色器(fragment shader)
;
顶点着色器:是处理顶点信息的GPU代码片段,可以用于顶点坐标的变换、法线变换与归一化、纹理坐标生成与变换、光照等信息;
片元着色器:WebGL 从顶点着色器和图元提取像素点给片元着色器执行代码的过程,就是我们前面说的生成光栅信息的过程,我们也叫它光栅化过程。所以,片元着色器的作用,就是处理光栅化后的像素信息。
用途:运算与插值、纹理访问、纹理应用、Fog、色彩应用;
或者说,片元着色器
的功能就是计算出当前基元的每一个像素点的颜色信息;
着色器
接收收据的4种方式:
所有你想要通过顶点着色器和片元着色器拿到的信息必须要提供给GPU,着色器有四种方式可以获取到这些绘制所需的数据:
Attributes and Buffers
:Buffer
通常是你上传到GPU的二进制格式的数组,通常Buffer
包含位置、法线、纹理坐标、顶点颜色等信息,当然你可以给Buffer
传入任何你想要的值。Attributes
用于定义如何从Buffer
中拉取数据并把它们提供给顶点着色器。比如你在Buffer
中存放了一些位置信息,每个位置用一个三维的32位的浮点数表示。你需要告诉特定的attribute
应该从哪个Buffer
中去获取这些位置信息,以什么样的数据格式去拉取这些信息,这些位置信息的起始地址是什么,每个位置信息的大小又是多少。Buffer
不是随机存取的。相反,一些顶点着色器执行特定的次数,每次执行时,都会从每个指定的缓冲区取出下一个顶点值值并赋给一个attribute
。Uniforms
:是在执行shader
程序前设定的高效的全局变量,在单次绘制调用中对所有的顶点保持一致;比如平移变换时为所有顶点平移同样的距离;Textures
:是shader
程序中可以随机存取的数据信息,Textures
中通常存储的是图像数据,但是只是不包括图像的颜色信息;Varyings
:是顶点着色器向片元着色器传递数据的一种方式,比如在顶点着色器中定义与位置信息关联的颜色变量,然后将其传给片元着色器用于后期绘制。根据基元类型(点、线、三角形),在顶点着色器中由varying
设定的变量值在片元着色器执行过程中会被插值;
- 函数
每个shader
函数都必须有main()
函数,返回值为void时函数类型void必须指明;
GLSL函数是全局声明和定义的,因此不能在函数中声明一个新的函数,也不支持递归操作;
void main() {
...
}
- 常用API介绍
WebGLRenderingContext.createShader()
—— 创建shader对象
const canvas = canvasRef.current;
const gl = canvas.getContext("webgl");
const shader = gl.createShader(type); // type可取:gl.VERTEX_SHADER or gl.FRAGMENT_SHADER
WebGLRenderingContext.shaderSource()
——
gl.shaderSource(shader, source),其中shader
为gl.createShader
创建的对象,source
为包含GLSL源代码的DOMString
;WebGLRenderingContext.compileShader()
—— 把GLSL shader编译为二进制数据供WebGL使用。
gl.compileShader(shader);WebGLRenderingContext.createProgram()
—— 创建WebGLProgram
对象WebGLRenderingContext.attachShader()
—— 将着色器对象与WebGLProgram
对象关联;
void gl.attachShader(program, shader);WebGLRenderingContext.linkProgram()
—— 链接一个给定的WebGLProgram,完成为程序片段和顶点着色器准备GPU代码的过程;
void gl.linkProgram(program);WebGLRenderingContext.useProgram()
—— 为当前渲染阶段指定WebGLProgram
对象;
void gl.useProgram(program);WebGLRenderingContext.createBuffer()
—— 创建和初始化一个WebGLBuffer
对象用于存储顶点和颜色等信息;返回创建的WebGLBuffer
对象;WebGLRenderingContext.bindBuffer()
—— 为目标指定WebGLBuffer
对象;
void gl.bindBuffer(target, buffer);target
可以是gl.ARRAY_BUFFER
、gl.ELEMENT_ARRAY_BUFFER
等;WebGLRenderingContext.bufferData()
—— 为目标指定WebGLBuffer
对象;
// WebGL1:
void gl.bufferData(target, size, usage);
void gl.bufferData(target, ArrayBuffer? srcData, usage);
void gl.bufferData(target, ArrayBufferView srcData, usage);
// WebGL2:
void gl.bufferData(target, ArrayBufferView srcData, usage, srcOffset, length);
usage
是出于优化目的指定的数据存储方式,可选值包括:gl.STATIC_DRAW
、gl.DYNAMIC_DRAW
、gl.STREAM_DRAW
等;具体可参考此处
const bufferId = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, bufferId);
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW); // 指定数据存储区的使用方法,可以取值gl.DYNAMIC_DRAW、gl.STREAM_DRAW等
WebGLRenderingContext.getAttribLocation()
—— 获取指定WebGLProgram
对象中的某个attribute
变量的地址索引;
GLint gl.getAttribLocation(program, variableName);WebGLRenderingContext.vertexAttribPointer()
—— 获取顶点信息;
void gl.vertexAttribPointer(index, size, type, normalized, stride, offset);index
—— 待操作的vertex attribute
变量地址索引size
——vertex attribute
维数,取[1-4]type
—— 数据类型,取gl.BYTE
、gl.SHORT
、gl.FLOAT
等;normalized
—— 是否进行数据归一化处理,根据数据类型归一化为[-1, 1]或[0, 1]stride
—— 指定连续vertex attribute
开头之间的偏移量(以字节为单位),stride为0表示紧密打包,即强类型vertex数组中每行只包含一个属性;offset
—— 指定vertex attribute
数组中第一个分量的的偏移量(以字节为单位,是type
的整数倍)
比如纹理绘制中stride
为顶点数组中每行元素的byte数,包含webgl几何坐标及纹理坐标;此时offset
仅为几何坐标的字节数;WebGLRenderingContext.enableVertexAttribArray()
—— 激活顶点变量;
void gl.enableVertexAttribArray(index);WebGLRenderingContext.clear()
—— 清理画布,将buffer
重置为预设值,可能影响颜色、深度等信息;
void gl.clear(mask) ——mask
取值包括gl.COLOR_BUFFER_BIT
、gl.DEPTH_BUFFER_BIT
、gl.STENCIL_BUFFER_BIT
;WebGLRenderingContext.drawArrays()
—— 根据数组对象绘制图元;
void gl.drawArrays(mode, first, count);mode
—— 指定图元类型,包括gl.POINTS
(绘制单个点)、gl.LINE_STRIP
(绘制一条到下一个顶点的线段)、gl.LINE_LOOP
(与gl.LINE_STRIP
相似,但会首尾相连)、gl.LINES
(每两个顶点确定一个线段,顶点不共用)、gl.TRIANGLES
(绘制实体三角形,顶点不共用)、gl.TRIANGLE_STRIP
(绘制实体三角形,后两个顶点会与下一个三角形共用)、gl.TRIANGLE_FAN
(以第一个顶点为圆形绘制一系列三角形,第一个顶点共用,可以用作扇形绘制)start
—— 指定起始顶点索引count
—— 指定需要被渲染的所引数
简单图形绘制·
绘制一个三角形
import React, { useEffect, useRef } from "react";
import "./styles.css";
export default function App() {
const canvasRef = useRef();
useEffect(() => {
/**
* Step one: create webgl context
*/
const canvas = canvasRef.current;
const gl = canvas.getContext("webgl");
/**
* Step two: create webgl program
*/
const vertex = `
attribute vec2 position;
varying vec3 color;
void main() {
gl_PointSize = 1.0;
color = vec3(0.5 + position * 0.5, 0.0);
gl_Position = vec4(position, 1.0, 1.0);
}
`;
const fragment = `
precision mediump float;
varying vec3 color;
void main() {
gl_FragColor = vec4(color, 1.0);
}
`;
// Create Shader Object
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertex);
gl.compileShader(vertexShader);
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragment);
gl.compileShader(fragmentShader);
// create webgl program
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
/**
* Store data in buffer
*/
const points = new Float32Array([-1, -1, 0, 1, 1, -1]);
const bufferId = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, bufferId);
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW); // 指定数据存储区的使用方法,可以取值gl.DYNAMIC_DRAW、gl.STREAM_DRAW等
/**
* GPU read buffer data
*/
const vPosition = gl.getAttribLocation(program, "position"); // 获取position变量地址
gl.vertexAttribPointer(vPosition, 2, gl.FLOAT, false, 0, 0); // 从缓存区读取顶点信息,设定变量维数和数据类型
gl.enableVertexAttribArray(vPosition); // 激活变量
/**
* Excute Shader Program and Complete the drawing
*/
gl.clear(gl.COLOR_BUFFER_BIT); // 清理画布
gl.drawArrays(gl.TRIANGLES, 0, points.length / 2); // 绘制,传入绘制模式(gl.POINTS/gl.LINES/gl.LINES_STRIP/gl.LINES_LOOP)、起始顶点及顶点数
}, []);
return (
<div className="App">
<h1>WebGL example</h1>
<canvas id="canvas" width="400" height="400" ref={canvasRef} />
</div>
);
}
如果需要绘制一个实心的矩形,可以修改下写入缓存的顶点信息及gl绘制模式即可;基于上面的代码修改如下,具体实现点击此处
const points = new Float32Array([0, 0, 0, 1, 1, 0, 1, 1]);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, points.length / 2);
如果需要绘制一个空心的三角形,可以将gl.drawArrays
方法的第一个参数进行修改即可,具体源码参考此处。使用gl.LINE_LOOP
——该绘制模式以线段作为图元,表示将最后一个顶点与第一个顶点首尾相连。当然,你也可以试试gl.LINE_STRIP
(与gl.LINE_LOOP
类似,顶点公用,但是不会首尾相连)和gl.LINES
(每两个点表示一个线段,顶点不共用);
更加优雅的代码实现(来源于参考文献):
/* eslint no-console:0 consistent-return:0 */
"use strict";
function createShader(gl, type, source) {
var shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
var success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (success) {
return shader;
}
console.log(gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
}
function createProgram(gl, vertexShader, fragmentShader) {
var program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
var success = gl.getProgramParameter(program, gl.LINK_STATUS);
if (success) {
return program;
}
console.log(gl.getProgramInfoLog(program));
gl.deleteProgram(program);
}
function main() {
// Get A WebGL context
var canvas = document.querySelector("#c");
var gl = canvas.getContext("webgl");
if (!gl) {
return;
}
// Get the strings for our GLSL shaders
var vertexShaderSource = document.querySelector("#vertex-shader-2d").text;
var fragmentShaderSource = document.querySelector("#fragment-shader-2d").text;
// create GLSL shaders, upload the GLSL source, compile the shaders
var vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
var fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
// Link the two shaders into a program
var program = createProgram(gl, vertexShader, fragmentShader);
// look up where the vertex data needs to go.
var positionAttributeLocation = gl.getAttribLocation(program, "a_position");
// Create a buffer and put three 2d clip space points in it
var positionBuffer = gl.createBuffer();
// Bind it to ARRAY_BUFFER (think of it as ARRAY_BUFFER = positionBuffer)
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
var positions = [
0, 0,
0, 0.5,
0.7, 0,
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
// code above this line is initialization code.
// code below this line is rendering code.
webglUtils.resizeCanvasToDisplaySize(gl.canvas);
// Tell WebGL how to convert from clip space to pixels
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
// Clear the canvas
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
// Tell it to use our program (pair of shaders)
gl.useProgram(program);
// Turn on the attribute
gl.enableVertexAttribArray(positionAttributeLocation);
// Bind the position buffer.
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// Tell the attribute how to get data out of positionBuffer (ARRAY_BUFFER)
var size = 2; // 2 components per iteration
var type = gl.FLOAT; // the data is 32bit floats
var normalize = false; // don't normalize the data
var stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next position
var offset = 0; // start at the beginning of the buffer
gl.vertexAttribPointer(
positionAttributeLocation, size, type, normalize, stride, offset);
// draw
var primitiveType = gl.TRIANGLES;
var offset = 0;
var count = 3;
gl.drawArrays(primitiveType, offset, count);
}
main();
还有更多WebGL
实现的奇幻效果可以见此处
参考文献
- The OpenGL® Shading Language
- GLSL着色器
- How GPUs Work
- gl.bufferData
- GL_TRIANGLE_FAN Vs GL_TRIANGLE_STRIP
- glsl sandbox
- WebGL Fundamentals
- WebGL Shaders and GLSL