1. dot
import React, { useEffect, useRef } from 'react';
const WebGL = () => {
// 引用 canvas DOM,供 JS 操作绘图区域
const canvasRef = useRef<HTMLCanvasElement>(null);
// 编译单个着色器(顶点或片元),失败返回 null
function loadShader(gl: WebGL2RenderingContext, type: number, source: string) {
const shader = gl.createShader(type);
if (!shader) return null;
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(shader));
return null;
}
return shader;
}
// 将顶点 + 片元着色器链接成一个可执行的 GPU 程序
function initShaderProgram(
gl: WebGL2RenderingContext,
vertexShaderSource: string,
fragmentShaderSource: string,
) {
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vertexShaderSource); // 创建顶点着色器
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); // 创建片元着色器
const program = gl.createProgram(); // 创建程序对象
if (!program) return null;
gl.attachShader(program, vertexShader); // 附加顶点着色器
gl.attachShader(program, fragmentShader); // 附加片元着色器
gl.linkProgram(program); // 链接程序对象
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error(gl.getProgramInfoLog(program));
return null;
}
return program;
}
function initCanvas() {
const canvas = canvasRef.current;
if (!canvas) {
return;
}
// 获取 WebGL2 上下文,后续所有绘制通过 gl 操作
const gl = canvas.getContext('webgl2');
if (!gl) return;
// 清屏:设背景色(黑)并清空颜色缓冲区
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 顶点着色器:vec4(x, y, z, w) 裁剪空间 [-1, 1],x 横 y 纵 z 深度,w 齐次;gl_PointSize 点大小(像素),默认值为 1.0
// w 齐次:第 4 分量,GPU 显示前会把 x、y 除以 w 得到最终位置;2D 画图固定 w=1.0 即可
const vertexShaderSource = `#version 300 es
void main() {
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
gl_PointSize = 10.0;
}
`;
// 片元着色器:vec4(R, G, B, A),各分量取值 0.0~1.0,R 红 G 绿 B 蓝 A 透明度
// WebGL2 无 gl_FragColor,需声明 out 变量作为颜色输出
const fragmentShaderSource = `#version 300 es
precision mediump float;
out vec4 fragColor;
void main() {
fragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;
// 编译并链接着色器,得到 program(准备工作,此时还未绘制)
const program = initShaderProgram(gl, vertexShaderSource, fragmentShaderSource);
if (!program) return;
// 告诉 GPU 接下来使用这套着色器程序
gl.useProgram(program);
// 开始绘制:POINTS=画点,0=从第0个顶点开始,1=共画1个
gl.drawArrays(gl.POINTS, 0, 1);
}
// 组件挂载后初始化 WebGL2 并绘制
useEffect(() => {
initCanvas();
}, []);
return (
<div>
{/* width/height 为画布像素尺寸,非 CSS 尺寸 */}
<canvas ref={canvasRef} width="200" height="100"></canvas>
</div>
);
};
export default WebGL;
2. dot-with-mousedown
import React, { useEffect, useRef } from 'react';
const WebGL = () => {
// 引用 canvas DOM,供 JS 操作绘图区域
const canvasRef = useRef<HTMLCanvasElement>(null);
const glRef = useRef<WebGL2RenderingContext | null>(null);
const programRef = useRef<WebGLProgram | null>(null);
const positionsRef = useRef<[number, number, [number, number, number, number]][]>([]);
// 编译单个着色器(顶点或片元),失败返回 null
function loadShader(gl: WebGL2RenderingContext, type: number, source: string) {
const shader = gl.createShader(type);
if (!shader) return null;
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(shader));
return null;
}
return shader;
}
// 将顶点 + 片元着色器链接成一个可执行的 GPU 程序
function initShaderProgram(
gl: WebGL2RenderingContext,
vertexShaderSource: string,
fragmentShaderSource: string,
) {
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vertexShaderSource); // 创建顶点着色器
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); // 创建片元着色器
const program = gl.createProgram(); // 创建程序对象
if (!program) return null;
gl.attachShader(program, vertexShader); // 附加顶点着色器
gl.attachShader(program, fragmentShader); // 附加片元着色器
gl.linkProgram(program); // 链接程序对象
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error(gl.getProgramInfoLog(program));
return null;
}
return program;
}
// 像素坐标 → 裁剪空间 [-1, 1](WebGL 原点在中心,y 轴向上)
function pixelToClipSpace(x: number, y: number, width: number, height: number) {
const clipX = (x / width) * 2 - 1;
const clipY = 1 - (y / height) * 2;
return [clipX, clipY] as const;
}
function getColor() {
return [Math.random(), Math.random(), Math.random(), Math.random()] as [
number,
number,
number,
number,
];
}
function handleMouseDown(event: React.MouseEvent<HTMLCanvasElement>) {
const gl = glRef.current;
const program = programRef.current;
const canvas = canvasRef.current;
if (!gl || !program || !canvas) return;
const x = event.nativeEvent.offsetX;
const y = event.nativeEvent.offsetY;
const [clipX, clipY] = pixelToClipSpace(x, y, canvas.width, canvas.height);
const color = getColor();
positionsRef.current.push([clipX, clipY, color]);
gl.clearColor(0.0, 0.0, 0.0, 1.0); // 清屏:设背景色(黑)
gl.clear(gl.COLOR_BUFFER_BIT); // 把颜色缓冲区里每个像素都重写成该颜色(整屏清空 / 清屏)
// JS 只能写入顶点 attribute;片元着色器的数据须由顶点着色器 out 转发
const aPosition = gl.getAttribLocation(program, 'a_Position');
const aColor = gl.getAttribLocation(program, 'a_Color');
positionsRef.current.forEach(([x, y, color]) => {
gl.vertexAttrib4f(aColor, ...color); // → 顶点 in a_Color → out f_Color → 片元 in f_Color
gl.vertexAttrib2f(aPosition, x, y);
gl.drawArrays(gl.POINTS, 0, 1);
});
}
function initCanvas() {
const canvas = canvasRef.current;
if (!canvas) {
return;
}
// 获取 WebGL2 上下文,后续所有绘制通过 gl 操作
const gl = canvas.getContext('webgl2');
if (!gl) return;
// 清屏:设背景色(黑)并清空颜色缓冲区
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 顶点着色器:接收 JS 传入的 attribute,并负责把需交给片元的变量用 out 声明转发
const vertexShaderSource = `#version 300 es
in vec2 a_Position; // 顶点位置(裁剪空间),JS 通过 vertexAttrib 写入
in vec4 a_Color; // 顶点颜色,JS 通过 vertexAttrib 写入
out vec4 f_Color; // 转发给片元着色器(片元侧同名 in 接收,不可跳过本阶段)
void main() {
gl_Position = vec4(a_Position, 0.0, 1.0);
gl_PointSize = 10.0;
f_Color = a_Color; // 片元无法直接读 attribute,须经此处 out 传递
}
`;
// 片元着色器:只能接收顶点 out 对应的 in,不能直接读 JS attribute
// vec4(R,G,B,A) 各分量 0.0~1.0;WebGL2 无 gl_FragColor,需声明 out 输出最终颜色
const fragmentShaderSource = `#version 300 es
precision mediump float;
in vec4 f_Color; // 与顶点 out f_Color 配对,由 GPU 在图元内插值后传入
out vec4 fragColor;
void main() {
fragColor = f_Color;
}
`;
// 编译并链接着色器,得到 program(准备工作,此时还未绘制)
const program = initShaderProgram(gl, vertexShaderSource, fragmentShaderSource);
if (!program) return;
gl.useProgram(program);
glRef.current = gl;
programRef.current = program;
}
// 组件挂载后初始化 WebGL2 并绘制
useEffect(() => {
initCanvas();
}, []);
return (
<div>
{/* width/height 为画布像素尺寸,非 CSS 尺寸 */}
<canvas ref={canvasRef} width="200" height="100" onMouseDown={handleMouseDown}></canvas>
</div>
);
};
export default WebGL;
3. triangle
import React, { useEffect, useRef } from 'react';
const WebGL = () => {
// 引用 canvas DOM,供 JS 操作绘图区域
const canvasRef = useRef<HTMLCanvasElement>(null);
const glRef = useRef<WebGL2RenderingContext | null>(null);
const programRef = useRef<WebGLProgram | null>(null);
// 编译单个着色器(顶点或片元),失败返回 null
function loadShader(gl: WebGL2RenderingContext, type: number, source: string) {
const shader = gl.createShader(type);
if (!shader) return null;
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(shader));
return null;
}
return shader;
}
// 将顶点 + 片元着色器链接成一个可执行的 GPU 程序
function initShaderProgram(
gl: WebGL2RenderingContext,
vertexShaderSource: string,
fragmentShaderSource: string,
) {
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vertexShaderSource); // 创建顶点着色器
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); // 创建片元着色器
const program = gl.createProgram(); // 创建程序对象
if (!program) return null;
gl.attachShader(program, vertexShader); // 附加顶点着色器
gl.attachShader(program, fragmentShader); // 附加片元着色器
gl.linkProgram(program); // 链接程序对象
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error(gl.getProgramInfoLog(program));
return null;
}
return program;
}
function initCanvas() {
const canvas = canvasRef.current;
if (!canvas) {
return;
}
// 获取 WebGL2 上下文,后续所有绘制通过 gl 操作
const gl = canvas.getContext('webgl2');
if (!gl) return;
// 清屏:设背景色(黑)并清空颜色缓冲区
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 顶点着色器:接收 JS 传入的 attribute,并负责把需交给片元的变量用 out 声明转发
const vertexShaderSource = `#version 300 es
in vec2 a_Position; // 顶点位置(裁剪空间),JS 通过 buffer 写入
in vec4 a_Color; // 顶点颜色,JS 通过 buffer 写入
out vec4 f_Color; // 转发给片元着色器(片元侧同名 in 接收,不可跳过本阶段)
void main() {
gl_Position = vec4(a_Position, 0.0, 1.0);
f_Color = a_Color; // 片元无法直接读 attribute,须经此处 out 传递
}
`;
// 片元着色器:只能接收顶点 out 对应的 in,不能直接读 JS attribute
// vec4(R,G,B,A) 各分量 0.0~1.0;WebGL2 无 gl_FragColor,需声明 out 输出最终颜色
const fragmentShaderSource = `#version 300 es
precision mediump float;
in vec4 f_Color; // 与顶点 out f_Color 配对,由 GPU 在三角形内插值后传入
out vec4 fragColor;
void main() {
fragColor = f_Color;
}
`;
// 编译并链接着色器,得到 program(准备工作,此时还未绘制)
const program = initShaderProgram(gl, vertexShaderSource, fragmentShaderSource);
if (!program) return;
gl.useProgram(program);
// ── 顶点数据:buffer 路径(三角形一次 draw 需多个不同顶点) ──
// WebGL 有两种向 attribute 供数方式:
// 1) vertexAttrib*f:写入 GPU 状态,作为「常量 attribute」;适合 drawArrays(..., 1) 每次只画 1 个顶点(见 webgl-2-dot-with-mousedown)
// 2) bufferData + vertexAttribPointer:vertices 在 JS 内存,bufferData 上传到 GPU 显存;适合一次 draw 多个不同顶点(本例 TRIANGLES, 3)
// vertices 只是 CPU 侧定义,GPU 绘制时读不到 JS 变量,必须经过 createBuffer → bindBuffer → bufferData 上传。
const vertices = new Float32Array([
0.0,
0.5,
1.0,
0.0,
0.0,
1.0, // 上顶点,红
-0.5,
-0.5,
0.0,
1.0,
0.0,
1.0, // 左下,绿
0.5,
-0.5,
0.0,
0.0,
1.0,
1.0, // 右下,蓝
]); // 内存布局:[x y r g b a | x y r g b a | ...],aPosition 读前 2 个,aColor 读后 4 个
const buffer = gl.createBuffer(); // 在 GPU 侧创建空 buffer
gl.bindBuffer(gl.ARRAY_BUFFER, buffer); // 绑定为当前顶点缓冲区,后续 bufferData / vertexAttribPointer 都作用于此
// bufferData:把 vertices 从 JS 内存复制到 GPU 显存(「定义数据」≠「上传数据」,二者缺一不可)
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
const stride = 6 * Float32Array.BYTES_PER_ELEMENT; // 步长:6 个 float = (x, y, r, g, b, a)
const aPosition = gl.getAttribLocation(program, 'a_Position'); // 获取顶点着色器中的 a_Position 变量位置
const aColor = gl.getAttribLocation(program, 'a_Color'); // 获取顶点着色器中的 a_Color 变量位置
// vertexAttribPointer:告诉 GPU 如何从已绑定的 ARRAY_BUFFER 解析 attribute(不传输数据,只设读取规则)
// 参数:location, 分量数, 类型, 是否归一化, stride, offset
// 须配合 enableVertexAttribArray 启用「从 buffer 读」;若用 vertexAttrib*f 则无需 enable,且 attribute 对该次 draw 内所有顶点相同
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, stride, 0); // offset 0:读 x,y → in a_Position
gl.enableVertexAttribArray(aPosition);
gl.vertexAttribPointer(aColor, 4, gl.FLOAT, false, stride, 2 * Float32Array.BYTES_PER_ELEMENT); // offset 8:跳过 x,y,读 r,g,b,a → in a_Color → out f_Color → in f_Color
gl.enableVertexAttribArray(aColor);
// 一次提交 3 个顶点;GPU 按 vertexAttribPointer 规则从 buffer 逐个读取 → 顶点着色器 → 片元插值 f_Color
gl.drawArrays(gl.TRIANGLES, 0, 3);
glRef.current = gl;
programRef.current = program;
}
// 组件挂载后初始化 WebGL2 并绘制
useEffect(() => {
initCanvas();
}, []);
return (
<div>
{/* width/height 为画布像素尺寸,非 CSS 尺寸 */}
<canvas ref={canvasRef} width="200" height="100"></canvas>
</div>
);
};
export default WebGL;
4. auto-triangle
import React, { useEffect, useRef } from 'react';
const WebGL = () => {
// 引用 canvas DOM,供 JS 操作绘图区域
const canvasRef = useRef<HTMLCanvasElement>(null);
const glRef = useRef<WebGL2RenderingContext | null>(null);
const programRef = useRef<WebGLProgram | null>(null);
// 累积所有点击顶点,每 3 次点击组成 1 个三角形;每次 mousedown 重传 buffer 并重绘全部几何
const pointsRef = useRef<[number, number, number, number, number, number][]>([]);
function handleMouseDown(event: React.MouseEvent<HTMLCanvasElement>) {
// 坐标转换:offsetX/Y 相对 canvas 左上角(勿用 clientX/Y,canvas 不在页面原点时会偏移)
// 裁剪空间 x∈[-1,1];y 需翻转(屏幕 Y 向下,WebGL Y 向上)
const x = event.nativeEvent.offsetX;
const y = event.nativeEvent.offsetY;
pointsRef.current.push([
(x / canvasRef.current.width) * 2 - 1,
-(y / canvasRef.current.height) * 2 + 1,
Math.random(),
Math.random(),
Math.random(),
Math.random(),
]);
const flatPoints = pointsRef.current.flat();
const len = pointsRef.current.length;
const remainder = len % 3; // 当前轮次未凑齐三角形的余数顶点数(0/1/2)
const completeCount = len - remainder; // 可组成完整三角形的顶点数(3 的倍数)
const gl = glRef.current;
if (!gl) return;
const program = programRef.current;
if (!program) return;
const aPosition = gl.getAttribLocation(program, 'a_Position');
const aColor = gl.getAttribLocation(program, 'a_Color');
// buffer 路径:CPU 侧 vertices 须经 createBuffer → bindBuffer → bufferData 上传,GPU 才能读取
// 内存布局 [x y r g b a | x y r g b a | ...],stride=6 float;aPosition 读前 2 个,aColor 读后 4 个
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(flatPoints), gl.STATIC_DRAW);
const stride = 6 * Float32Array.BYTES_PER_ELEMENT;
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, stride, 0);
gl.enableVertexAttribArray(aPosition);
gl.vertexAttribPointer(aColor, 4, gl.FLOAT, false, stride, 2 * Float32Array.BYTES_PER_ELEMENT);
gl.enableVertexAttribArray(aColor);
// drawArrays(mode, first, count):first=起始顶点索引,count=读取的顶点个数(不是三角形个数)
// TRIANGLES 模式:顶点按顺序每 3 个一组构成一个三角形,一次 draw 可画 count/3 个三角形
// 顶点 0,1,2 → △1 | 3,4,5 → △2 | 6,7,8 → △3 ...
if (completeCount >= 3) {
gl.drawArrays(gl.TRIANGLES, 0, completeCount);
}
// 未完成顶点的预览(first 必须从 buffer 末尾读,first=0 会永远画第一次点击的位置)
// len%3===1(第 1/4/7... 次点击)→ 画最新 1 个点
// len%3===2(第 2/5/8... 次点击)→ 画最新 2 点之间的线
// len%3===0(第 3/6/9... 次点击)→ 已凑齐三角形,无需预览
if (remainder === 1) {
gl.drawArrays(gl.POINTS, len - 1, 1);
}
if (remainder === 2) {
gl.drawArrays(gl.LINES, len - 2, 2);
}
}
// 编译单个着色器(顶点或片元),失败返回 null
function loadShader(gl: WebGL2RenderingContext, type: number, source: string) {
const shader = gl.createShader(type);
if (!shader) return null;
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(shader));
return null;
}
return shader;
}
// 将顶点 + 片元着色器链接成一个可执行的 GPU 程序
function initShaderProgram(
gl: WebGL2RenderingContext,
vertexShaderSource: string,
fragmentShaderSource: string,
) {
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vertexShaderSource); // 创建顶点着色器
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); // 创建片元着色器
const program = gl.createProgram(); // 创建程序对象
if (!program) return null;
gl.attachShader(program, vertexShader); // 附加顶点着色器
gl.attachShader(program, fragmentShader); // 附加片元着色器
gl.linkProgram(program); // 链接程序对象
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error(gl.getProgramInfoLog(program));
return null;
}
return program;
}
function initCanvas() {
const canvas = canvasRef.current;
if (!canvas) {
return;
}
// 获取 WebGL2 上下文,后续所有绘制通过 gl 操作
const gl = canvas.getContext('webgl2');
if (!gl) return;
// 清屏:设背景色(黑)并清空颜色缓冲区
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 顶点着色器:接收 JS 传入的 attribute,并负责把需交给片元的变量用 out 声明转发
const vertexShaderSource = `#version 300 es
in vec2 a_Position; // 顶点位置(裁剪空间),JS 通过 buffer 写入
in vec4 a_Color; // 顶点颜色,JS 通过 buffer 写入
out vec4 f_Color; // 转发给片元着色器(片元侧同名 in 接收,不可跳过本阶段)
void main() {
gl_Position = vec4(a_Position, 0.0, 1.0);
gl_PointSize = 5.0; // POINTS 模式必须设置,否则点可能只有 1 像素或不可见
f_Color = a_Color; // 片元无法直接读 attribute,须经此处 out 传递
}
`;
// 片元着色器:只能接收顶点 out 对应的 in,不能直接读 JS attribute
// vec4(R,G,B,A) 各分量 0.0~1.0;WebGL2 无 gl_FragColor,需声明 out 输出最终颜色
const fragmentShaderSource = `#version 300 es
precision mediump float;
in vec4 f_Color; // 与顶点 out f_Color 配对,由 GPU 在三角形内插值后传入
out vec4 fragColor;
void main() {
fragColor = f_Color;
}
`;
// 编译并链接着色器,得到 program(准备工作,此时还未绘制)
const program = initShaderProgram(gl, vertexShaderSource, fragmentShaderSource);
if (!program) return;
gl.useProgram(program);
glRef.current = gl;
programRef.current = program;
}
// 组件挂载后初始化 WebGL2 并绘制
useEffect(() => {
initCanvas();
}, []);
return (
<div>
{/* width/height 为画布像素尺寸,非 CSS 尺寸 */}
<canvas ref={canvasRef} width="600" height="400" onMouseDown={handleMouseDown}></canvas>
</div>
);
};
export default WebGL;
5. circle
import React, { useEffect, useRef } from 'react';
const getX = (x: number, width: number) => {
return (x / width) * 2 - 1;
};
const getY = (y: number, height: number) => {
return -(y / height) * 2 + 1;
};
const WebGL = () => {
// 引用 canvas DOM,供 JS 操作绘图区域
const canvasRef = useRef<HTMLCanvasElement>(null);
const glRef = useRef<WebGL2RenderingContext | null>(null);
const programRef = useRef<WebGLProgram | null>(null);
// 编译单个着色器(顶点或片元),失败返回 null
function loadShader(gl: WebGL2RenderingContext, type: number, source: string) {
const shader = gl.createShader(type);
if (!shader) return null;
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(shader));
return null;
}
return shader;
}
// 将顶点 + 片元着色器链接成一个可执行的 GPU 程序
function initShaderProgram(
gl: WebGL2RenderingContext,
vertexShaderSource: string,
fragmentShaderSource: string,
) {
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vertexShaderSource); // 创建顶点着色器
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); // 创建片元着色器
const program = gl.createProgram(); // 创建程序对象
if (!program) return null;
gl.attachShader(program, vertexShader); // 附加顶点着色器
gl.attachShader(program, fragmentShader); // 附加片元着色器
gl.linkProgram(program); // 链接程序对象
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error(gl.getProgramInfoLog(program));
return null;
}
return program;
}
function initCanvas() {
const canvas = canvasRef.current;
if (!canvas) {
return;
}
// 获取 WebGL2 上下文,后续所有绘制通过 gl 操作
const gl = canvas.getContext('webgl2');
if (!gl) return;
// 清屏:设背景色(黑)并清空颜色缓冲区
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 顶点着色器:接收 JS 传入的 attribute,并负责把需交给片元的变量用 out 声明转发
const vertexShaderSource = `#version 300 es
in vec2 a_Position; // 顶点位置(裁剪空间),JS 通过 buffer 写入
in vec4 a_Color; // 顶点颜色,JS 通过 buffer 写入
out vec4 f_Color; // 转发给片元着色器(片元侧同名 in 接收,不可跳过本阶段)
void main() {
gl_Position = vec4(a_Position, 0.0, 1.0);
gl_PointSize = 5.0; // POINTS 模式必须设置,否则点可能只有 1 像素或不可见
f_Color = a_Color; // 片元无法直接读 attribute,须经此处 out 传递
}
`;
// 片元着色器:只能接收顶点 out 对应的 in,不能直接读 JS attribute
// vec4(R,G,B,A) 各分量 0.0~1.0;WebGL2 无 gl_FragColor,需声明 out 输出最终颜色
const fragmentShaderSource = `#version 300 es
precision mediump float;
in vec4 f_Color; // 与顶点 out f_Color 配对,由 GPU 在三角形内插值后传入
out vec4 fragColor;
void main() {
fragColor = f_Color;
}
`;
// 编译并链接着色器,得到 program(准备工作,此时还未绘制)
const program = initShaderProgram(gl, vertexShaderSource, fragmentShaderSource);
if (!program) return;
gl.useProgram(program);
glRef.current = gl;
programRef.current = program;
renderCircle(300, 200, 100, 50);
}
function renderCircle(x: number, y: number, radius: number, segments: number) {
const gl = glRef.current;
if (!gl) return;
const program = programRef.current;
if (!program) return;
const aPosition = gl.getAttribLocation(program, 'a_Position');
const aColor = gl.getAttribLocation(program, 'a_Color');
const color = [1.0, 0.0, 0.0, 1.0] as const;
const points: number[] = [];
for (let i = 0; i < segments; i++) {
const angle = (i / segments) * 2 * Math.PI;
points.push(
getX(x + radius * Math.cos(angle), canvasRef.current.width),
getY(y + radius * Math.sin(angle), canvasRef.current.height),
);
}
const buffer = gl.createBuffer();
points.push(points[0], points[1]);
points.unshift(getX(x, canvasRef.current.width), getY(y, canvasRef.current.height));
console.log(points);
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(points), gl.STATIC_DRAW);
const stride = 2 * Float32Array.BYTES_PER_ELEMENT;
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, stride, 0);
gl.enableVertexAttribArray(aPosition);
// 设置color为固定值
gl.disableVertexAttribArray(aColor); // 关键
gl.vertexAttrib4f(aColor, ...color);
gl.drawArrays(gl.TRIANGLE_FAN, 0, points.length / 2);
}
// 组件挂载后初始化 WebGL2 并绘制
useEffect(() => {
initCanvas();
}, []);
return (
<div>
{/* width/height 为画布像素尺寸,非 CSS 尺寸 */}
<canvas ref={canvasRef} width="600" height="400"></canvas>
</div>
);
};
export default WebGL;
6. auto-circle
import React, { useEffect, useRef } from 'react';
const getX = (x: number, width: number) => {
return (x / width) * 2 - 1;
};
const getY = (y: number, height: number) => {
return -(y / height) * 2 + 1;
};
/**
* 随机半径:2-10
*/
const randomRadius = () => {
return Math.ceil(Math.random() * 8) + 2;
};
const WebGL = () => {
// 引用 canvas DOM,供 JS 操作绘图区域
const canvasRef = useRef<HTMLCanvasElement>(null);
const glRef = useRef<WebGL2RenderingContext | null>(null);
const programRef = useRef<WebGLProgram | null>(null);
const circleRef = useRef<
{ points: number[]; radius: number; color: [number, number, number, number] }[]
>([
// { points: [], radius: 5, color: [1.0, 0.0, 0.0, 1.0] },
]);
// 编译单个着色器(顶点或片元),失败返回 null
function loadShader(gl: WebGL2RenderingContext, type: number, source: string) {
const shader = gl.createShader(type);
if (!shader) return null;
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(shader));
return null;
}
return shader;
}
// 将顶点 + 片元着色器链接成一个可执行的 GPU 程序
function initShaderProgram(
gl: WebGL2RenderingContext,
vertexShaderSource: string,
fragmentShaderSource: string,
) {
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vertexShaderSource); // 创建顶点着色器
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); // 创建片元着色器
const program = gl.createProgram(); // 创建程序对象
if (!program) return null;
gl.attachShader(program, vertexShader); // 附加顶点着色器
gl.attachShader(program, fragmentShader); // 附加片元着色器
gl.linkProgram(program); // 链接程序对象
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error(gl.getProgramInfoLog(program));
return null;
}
return program;
}
function initCanvas() {
const canvas = canvasRef.current;
if (!canvas) {
return;
}
// 获取 WebGL2 上下文,后续所有绘制通过 gl 操作
const gl = canvas.getContext('webgl2');
if (!gl) return;
// 清屏:设背景色(黑)并清空颜色缓冲区
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 顶点着色器:接收 JS 传入的 attribute,并负责把需交给片元的变量用 out 声明转发
const vertexShaderSource = `#version 300 es
in vec2 a_Position; // 顶点位置(裁剪空间),JS 通过 buffer 写入
in vec4 a_Color; // 顶点颜色,JS 通过 buffer 写入
out vec4 f_Color; // 转发给片元着色器(片元侧同名 in 接收,不可跳过本阶段)
void main() {
gl_Position = vec4(a_Position, 0.0, 1.0);
gl_PointSize = 5.0; // POINTS 模式必须设置,否则点可能只有 1 像素或不可见
f_Color = a_Color; // 片元无法直接读 attribute,须经此处 out 传递
}
`;
// 片元着色器:只能接收顶点 out 对应的 in,不能直接读 JS attribute
// vec4(R,G,B,A) 各分量 0.0~1.0;WebGL2 无 gl_FragColor,需声明 out 输出最终颜色
const fragmentShaderSource = `#version 300 es
precision mediump float;
in vec4 f_Color; // 与顶点 out f_Color 配对,由 GPU 在三角形内插值后传入
out vec4 fragColor;
void main() {
fragColor = f_Color;
}
`;
// 编译并链接着色器,得到 program(准备工作,此时还未绘制)
const program = initShaderProgram(gl, vertexShaderSource, fragmentShaderSource);
if (!program) return;
gl.useProgram(program);
glRef.current = gl;
programRef.current = program;
}
function renderCircle(
x: number,
y: number,
radius: number,
segments: number,
color: [number, number, number, number],
) {
const gl = glRef.current;
if (!gl) return;
const program = programRef.current;
if (!program) return;
const aPosition = gl.getAttribLocation(program, 'a_Position');
const aColor = gl.getAttribLocation(program, 'a_Color');
const points: number[] = [];
for (let i = 0; i < segments; i++) {
const angle = (i / segments) * 2 * Math.PI;
points.push(
getX(x + radius * Math.cos(angle), canvasRef.current.width),
getY(y + radius * Math.sin(angle), canvasRef.current.height),
);
}
const buffer = gl.createBuffer();
// 闭合路径
points.push(points[0], points[1]);
// 添加中心点
points.unshift(getX(x, canvasRef.current.width), getY(y, canvasRef.current.height));
// 绑定buffer
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(points), gl.STATIC_DRAW);
const stride = 2 * Float32Array.BYTES_PER_ELEMENT;
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, stride, 0);
gl.enableVertexAttribArray(aPosition);
// a_Color 二选一:enable 从 buffer 读(需 stride 含 rgba);disable 则用 vertexAttrib4f 常量
// buffer 仅有 x,y,须 disable,否则 vertexAttrib4f 被忽略,颜色/alpha 异常导致不可见
gl.disableVertexAttribArray(aColor);
gl.vertexAttrib4f(aColor, ...color);
// 绘制
gl.drawArrays(gl.TRIANGLE_FAN, 0, points.length / 2);
}
function handleMouseDown(event: React.MouseEvent<HTMLCanvasElement>) {
// 坐标转换:offsetX/Y 相对 canvas 左上角(勿用 clientX/Y,canvas 不在页面原点时会偏移)
// 裁剪空间 x∈[-1,1];y 需翻转(屏幕 Y 向下,WebGL Y 向上)
const x = event.nativeEvent.offsetX;
const y = event.nativeEvent.offsetY;
circleRef.current.push({
points: [x, y],
radius: randomRadius(),
color: [Math.random(), Math.random(), Math.random(), 1.0],
});
// 清屏:设背景色(黑)并清空颜色缓冲区
const gl = glRef.current;
if (!gl) return;
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
circleRef.current.forEach((circle) => {
renderCircle(circle.points[0], circle.points[1], circle.radius, 50, circle.color);
});
}
// 组件挂载后初始化 WebGL2 并绘制
useEffect(() => {
initCanvas();
}, []);
return (
<div>
{/* width/height 为画布像素尺寸,非 CSS 尺寸 */}
<canvas ref={canvasRef} width="600" height="400" onMouseDown={handleMouseDown}></canvas>
</div>
);
};
export default WebGL;
7. ring
import React, { useEffect, useRef } from 'react';
const getX = (x: number, width: number) => {
return (x / width) * 2 - 1;
};
const getY = (y: number, height: number) => {
return -(y / height) * 2 + 1;
};
const WebGL = () => {
// 引用 canvas DOM,供 JS 操作绘图区域
const canvasRef = useRef<HTMLCanvasElement>(null);
const glRef = useRef<WebGL2RenderingContext | null>(null);
const programRef = useRef<WebGLProgram | null>(null);
// 编译单个着色器(顶点或片元),失败返回 null
function loadShader(gl: WebGL2RenderingContext, type: number, source: string) {
const shader = gl.createShader(type);
if (!shader) return null;
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(shader));
return null;
}
return shader;
}
// 将顶点 + 片元着色器链接成一个可执行的 GPU 程序
function initShaderProgram(
gl: WebGL2RenderingContext,
vertexShaderSource: string,
fragmentShaderSource: string,
) {
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vertexShaderSource); // 创建顶点着色器
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); // 创建片元着色器
const program = gl.createProgram(); // 创建程序对象
if (!program) return null;
gl.attachShader(program, vertexShader); // 附加顶点着色器
gl.attachShader(program, fragmentShader); // 附加片元着色器
gl.linkProgram(program); // 链接程序对象
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error(gl.getProgramInfoLog(program));
return null;
}
return program;
}
function initCanvas() {
const canvas = canvasRef.current;
if (!canvas) {
return;
}
// 获取 WebGL2 上下文,后续所有绘制通过 gl 操作
const gl = canvas.getContext('webgl2');
if (!gl) return;
// 清屏:设背景色(黑)并清空颜色缓冲区
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 顶点着色器:接收 JS 传入的 attribute,并负责把需交给片元的变量用 out 声明转发
const vertexShaderSource = `#version 300 es
in vec2 a_Position; // 顶点位置(裁剪空间),JS 通过 buffer 写入
in vec4 a_Color; // 顶点颜色,JS 通过 buffer 写入
out vec4 f_Color; // 转发给片元着色器(片元侧同名 in 接收,不可跳过本阶段)
void main() {
gl_Position = vec4(a_Position, 0.0, 1.0);
gl_PointSize = 5.0; // POINTS 模式必须设置,否则点可能只有 1 像素或不可见
f_Color = a_Color; // 片元无法直接读 attribute,须经此处 out 传递
}
`;
// 片元着色器:只能接收顶点 out 对应的 in,不能直接读 JS attribute
// vec4(R,G,B,A) 各分量 0.0~1.0;WebGL2 无 gl_FragColor,需声明 out 输出最终颜色
const fragmentShaderSource = `#version 300 es
precision mediump float;
in vec4 f_Color; // 与顶点 out f_Color 配对,由 GPU 在三角形内插值后传入
out vec4 fragColor;
void main() {
fragColor = f_Color;
}
`;
// 编译并链接着色器,得到 program(准备工作,此时还未绘制)
const program = initShaderProgram(gl, vertexShaderSource, fragmentShaderSource);
if (!program) return;
gl.useProgram(program);
glRef.current = gl;
programRef.current = program;
renderCircle(300, 200, 100, 50);
}
function renderCircle(x: number, y: number, radius: number, segments: number) {
const gl = glRef.current;
if (!gl) return;
const program = programRef.current;
if (!program) return;
const aPosition = gl.getAttribLocation(program, 'a_Position');
const aColor = gl.getAttribLocation(program, 'a_Color');
const color = [1.0, 0.0, 0.0, 1.0] as const;
const points: number[] = [];
for (let i = 0; i < segments; i++) {
const angle = (i / segments) * 2 * Math.PI;
points.push(
getX(x + (radius / 2) * Math.cos(angle), canvasRef.current.width),
getY(y + (radius / 2) * Math.sin(angle), canvasRef.current.height),
getX(x + radius * Math.cos(angle), canvasRef.current.width),
getY(y + radius * Math.sin(angle), canvasRef.current.height),
);
}
const buffer = gl.createBuffer();
points.push(points[0], points[1], points[2], points[3]);
console.log(points);
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(points), gl.STATIC_DRAW);
const stride = 2 * Float32Array.BYTES_PER_ELEMENT;
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, stride, 0);
gl.enableVertexAttribArray(aPosition);
// 设置color为固定值
gl.disableVertexAttribArray(aColor); // 关键
gl.vertexAttrib4f(aColor, ...color);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, points.length / 2);
}
// 组件挂载后初始化 WebGL2 并绘制
useEffect(() => {
initCanvas();
}, []);
return (
<div>
{/* width/height 为画布像素尺寸,非 CSS 尺寸 */}
<canvas ref={canvasRef} width="600" height="400"></canvas>
</div>
);
};
export default WebGL;
8. auto-ring
import React, { useEffect, useRef } from 'react';
const getX = (x: number, width: number) => {
return (x / width) * 2 - 1;
};
const getY = (y: number, height: number) => {
return -(y / height) * 2 + 1;
};
/**
* 随机半径:2-10
*/
const randomRadius = () => {
return Math.ceil(Math.random() * 8) + 2;
};
const WebGL = () => {
// 引用 canvas DOM,供 JS 操作绘图区域
const canvasRef = useRef<HTMLCanvasElement>(null);
const glRef = useRef<WebGL2RenderingContext | null>(null);
const programRef = useRef<WebGLProgram | null>(null);
const circleRef = useRef<
{ points: number[]; radius: number; color: [number, number, number, number] }[]
>([
// { points: [], radius: 5, color: [1.0, 0.0, 0.0, 1.0] },
]);
// 编译单个着色器(顶点或片元),失败返回 null
function loadShader(gl: WebGL2RenderingContext, type: number, source: string) {
const shader = gl.createShader(type);
if (!shader) return null;
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(shader));
return null;
}
return shader;
}
// 将顶点 + 片元着色器链接成一个可执行的 GPU 程序
function initShaderProgram(
gl: WebGL2RenderingContext,
vertexShaderSource: string,
fragmentShaderSource: string,
) {
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vertexShaderSource); // 创建顶点着色器
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); // 创建片元着色器
const program = gl.createProgram(); // 创建程序对象
if (!program) return null;
gl.attachShader(program, vertexShader); // 附加顶点着色器
gl.attachShader(program, fragmentShader); // 附加片元着色器
gl.linkProgram(program); // 链接程序对象
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error(gl.getProgramInfoLog(program));
return null;
}
return program;
}
function initCanvas() {
const canvas = canvasRef.current;
if (!canvas) {
return;
}
// 获取 WebGL2 上下文,后续所有绘制通过 gl 操作
const gl = canvas.getContext('webgl2');
if (!gl) return;
// 清屏:设背景色(黑)并清空颜色缓冲区
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 顶点着色器:接收 JS 传入的 attribute,并负责把需交给片元的变量用 out 声明转发
const vertexShaderSource = `#version 300 es
in vec2 a_Position; // 顶点位置(裁剪空间),JS 通过 buffer 写入
in vec4 a_Color; // 顶点颜色,JS 通过 buffer 写入
out vec4 f_Color; // 转发给片元着色器(片元侧同名 in 接收,不可跳过本阶段)
void main() {
gl_Position = vec4(a_Position, 0.0, 1.0);
gl_PointSize = 5.0; // POINTS 模式必须设置,否则点可能只有 1 像素或不可见
f_Color = a_Color; // 片元无法直接读 attribute,须经此处 out 传递
}
`;
// 片元着色器:只能接收顶点 out 对应的 in,不能直接读 JS attribute
// vec4(R,G,B,A) 各分量 0.0~1.0;WebGL2 无 gl_FragColor,需声明 out 输出最终颜色
const fragmentShaderSource = `#version 300 es
precision mediump float;
in vec4 f_Color; // 与顶点 out f_Color 配对,由 GPU 在三角形内插值后传入
out vec4 fragColor;
void main() {
fragColor = f_Color;
}
`;
// 编译并链接着色器,得到 program(准备工作,此时还未绘制)
const program = initShaderProgram(gl, vertexShaderSource, fragmentShaderSource);
if (!program) return;
gl.useProgram(program);
glRef.current = gl;
programRef.current = program;
}
function renderRing(
x: number,
y: number,
radius: number,
segments: number,
color: [number, number, number, number],
) {
const gl = glRef.current;
if (!gl) return;
const program = programRef.current;
if (!program) return;
const aPosition = gl.getAttribLocation(program, 'a_Position');
const aColor = gl.getAttribLocation(program, 'a_Color');
const points: number[] = [];
for (let i = 0; i < segments; i++) {
const angle = (i / segments) * 2 * Math.PI;
points.push(
getX(x + (radius / 2) * Math.cos(angle), canvasRef.current.width),
getY(y + (radius / 2) * Math.sin(angle), canvasRef.current.height),
getX(x + radius * Math.cos(angle), canvasRef.current.width),
getY(y + radius * Math.sin(angle), canvasRef.current.height),
);
}
const buffer = gl.createBuffer();
// 闭合路径
points.push(points[0], points[1], points[2], points[3]);
// 绑定buffer
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(points), gl.STATIC_DRAW);
const stride = 2 * Float32Array.BYTES_PER_ELEMENT;
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, stride, 0);
gl.enableVertexAttribArray(aPosition);
// a_Color 二选一:enable 从 buffer 读(需 stride 含 rgba);disable 则用 vertexAttrib4f 常量
// buffer 仅有 x,y,须 disable,否则 vertexAttrib4f 被忽略,颜色/alpha 异常导致不可见
gl.disableVertexAttribArray(aColor);
gl.vertexAttrib4f(aColor, ...color);
// 绘制
gl.drawArrays(gl.TRIANGLE_STRIP, 0, points.length / 2);
}
function handleMouseDown(event: React.MouseEvent<HTMLCanvasElement>) {
// 坐标转换:offsetX/Y 相对 canvas 左上角(勿用 clientX/Y,canvas 不在页面原点时会偏移)
// 裁剪空间 x∈[-1,1];y 需翻转(屏幕 Y 向下,WebGL Y 向上)
const x = event.nativeEvent.offsetX;
const y = event.nativeEvent.offsetY;
circleRef.current.push({
points: [x, y],
radius: randomRadius(),
color: [Math.random(), Math.random(), Math.random(), 1.0],
});
// 清屏:设背景色(黑)并清空颜色缓冲区
const gl = glRef.current;
if (!gl) return;
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
circleRef.current.forEach((circle) => {
renderRing(circle.points[0], circle.points[1], circle.radius, 50, circle.color);
});
}
// 组件挂载后初始化 WebGL2 并绘制
useEffect(() => {
initCanvas();
}, []);
return (
<div>
{/* width/height 为画布像素尺寸,非 CSS 尺寸 */}
<canvas ref={canvasRef} width="600" height="400" onMouseDown={handleMouseDown}></canvas>
</div>
);
};
export default WebGL;
9. rectangle
import React, { useEffect, useRef } from 'react';
const getX = (x: number, width: number) => {
return (x / width) * 2 - 1;
};
const getY = (y: number, height: number) => {
return -(y / height) * 2 + 1;
};
const WebGL = () => {
// 引用 canvas DOM,供 JS 操作绘图区域
const canvasRef = useRef<HTMLCanvasElement>(null);
const glRef = useRef<WebGL2RenderingContext | null>(null);
const programRef = useRef<WebGLProgram | null>(null);
// 编译单个着色器(顶点或片元),失败返回 null
function loadShader(gl: WebGL2RenderingContext, type: number, source: string) {
const shader = gl.createShader(type);
if (!shader) return null;
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(shader));
return null;
}
return shader;
}
// 将顶点 + 片元着色器链接成一个可执行的 GPU 程序
function initShaderProgram(
gl: WebGL2RenderingContext,
vertexShaderSource: string,
fragmentShaderSource: string,
) {
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vertexShaderSource); // 创建顶点着色器
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); // 创建片元着色器
const program = gl.createProgram(); // 创建程序对象
if (!program) return null;
gl.attachShader(program, vertexShader); // 附加顶点着色器
gl.attachShader(program, fragmentShader); // 附加片元着色器
gl.linkProgram(program); // 链接程序对象
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error(gl.getProgramInfoLog(program));
return null;
}
return program;
}
function initCanvas() {
const canvas = canvasRef.current;
if (!canvas) {
return;
}
// 获取 WebGL2 上下文,后续所有绘制通过 gl 操作
const gl = canvas.getContext('webgl2');
if (!gl) return;
// 清屏:设背景色(黑)并清空颜色缓冲区
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 顶点着色器:接收 JS 传入的 attribute,并负责把需交给片元的变量用 out 声明转发
const vertexShaderSource = `#version 300 es
in vec2 a_Position; // 顶点位置(裁剪空间),JS 通过 buffer 写入
in vec4 a_Color; // 顶点颜色,JS 通过 buffer 写入
out vec4 f_Color; // 转发给片元着色器(片元侧同名 in 接收,不可跳过本阶段)
void main() {
gl_Position = vec4(a_Position, 0.0, 1.0);
gl_PointSize = 5.0; // POINTS 模式必须设置,否则点可能只有 1 像素或不可见
f_Color = a_Color; // 片元无法直接读 attribute,须经此处 out 传递
}
`;
// 片元着色器:只能接收顶点 out 对应的 in,不能直接读 JS attribute
// vec4(R,G,B,A) 各分量 0.0~1.0;WebGL2 无 gl_FragColor,需声明 out 输出最终颜色
const fragmentShaderSource = `#version 300 es
precision mediump float;
in vec4 f_Color; // 与顶点 out f_Color 配对,由 GPU 在三角形内插值后传入
out vec4 fragColor;
void main() {
fragColor = f_Color;
}
`;
// 编译并链接着色器,得到 program(准备工作,此时还未绘制)
const program = initShaderProgram(gl, vertexShaderSource, fragmentShaderSource);
if (!program) return;
gl.useProgram(program);
glRef.current = gl;
programRef.current = program;
renderRectangle(300, 200, 100, 50);
}
function renderRectangle(x: number, y: number, width: number, height: number) {
const gl = glRef.current;
if (!gl) return;
const program = programRef.current;
if (!program) return;
const aPosition = gl.getAttribLocation(program, 'a_Position');
const aColor = gl.getAttribLocation(program, 'a_Color');
const color = [1.0, 0.0, 0.0, 1.0] as const;
const points: number[] = [];
points.push(
getX(x + width / 2, canvasRef.current.width),
getY(y - height / 2, canvasRef.current.height),
getX(x + width / 2, canvasRef.current.width),
getY(y + height / 2, canvasRef.current.height),
getX(x - width / 2, canvasRef.current.width),
getY(y - height / 2, canvasRef.current.height),
getX(x - width / 2, canvasRef.current.width),
getY(y + height / 2, canvasRef.current.height),
);
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(points), gl.STATIC_DRAW);
const stride = 2 * Float32Array.BYTES_PER_ELEMENT;
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, stride, 0);
gl.enableVertexAttribArray(aPosition);
// 设置color为固定值
gl.disableVertexAttribArray(aColor); // 关键
gl.vertexAttrib4f(aColor, ...color);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, points.length / 2);
}
// 组件挂载后初始化 WebGL2 并绘制
useEffect(() => {
initCanvas();
}, []);
return (
<div>
{/* width/height 为画布像素尺寸,非 CSS 尺寸 */}
<canvas ref={canvasRef} width="600" height="400"></canvas>
</div>
);
};
export default WebGL;
10. auto-rectangle
import React, { useEffect, useRef } from 'react';
const getX = (x: number, width: number) => {
return (x / width) * 2 - 1;
};
const getY = (y: number, height: number) => {
return -(y / height) * 2 + 1;
};
/**
* 随机宽度:5-13
* 随机高度:5-13
*/
const randomWidthAndHeight = () => {
return Math.ceil(Math.random() * 8) + 5;
};
const WebGL = () => {
// 引用 canvas DOM,供 JS 操作绘图区域
const canvasRef = useRef<HTMLCanvasElement>(null);
const glRef = useRef<WebGL2RenderingContext | null>(null);
const programRef = useRef<WebGLProgram | null>(null);
const rectangleRef = useRef<
{ points: number[]; width: number; height: number; color: [number, number, number, number] }[]
>([
// { points: [], width: 5, height: 5, color: [1.0, 0.0, 0.0, 1.0] },
]);
// 编译单个着色器(顶点或片元),失败返回 null
function loadShader(gl: WebGL2RenderingContext, type: number, source: string) {
const shader = gl.createShader(type);
if (!shader) return null;
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(shader));
return null;
}
return shader;
}
// 将顶点 + 片元着色器链接成一个可执行的 GPU 程序
function initShaderProgram(
gl: WebGL2RenderingContext,
vertexShaderSource: string,
fragmentShaderSource: string,
) {
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vertexShaderSource); // 创建顶点着色器
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); // 创建片元着色器
const program = gl.createProgram(); // 创建程序对象
if (!program) return null;
gl.attachShader(program, vertexShader); // 附加顶点着色器
gl.attachShader(program, fragmentShader); // 附加片元着色器
gl.linkProgram(program); // 链接程序对象
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error(gl.getProgramInfoLog(program));
return null;
}
return program;
}
function initCanvas() {
const canvas = canvasRef.current;
if (!canvas) {
return;
}
// 获取 WebGL2 上下文,后续所有绘制通过 gl 操作
const gl = canvas.getContext('webgl2');
if (!gl) return;
// 清屏:设背景色(黑)并清空颜色缓冲区
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 顶点着色器:接收 JS 传入的 attribute,并负责把需交给片元的变量用 out 声明转发
const vertexShaderSource = `#version 300 es
in vec2 a_Position; // 顶点位置(裁剪空间),JS 通过 buffer 写入
in vec4 a_Color; // 顶点颜色,JS 通过 buffer 写入
out vec4 f_Color; // 转发给片元着色器(片元侧同名 in 接收,不可跳过本阶段)
void main() {
gl_Position = vec4(a_Position, 0.0, 1.0);
gl_PointSize = 5.0; // POINTS 模式必须设置,否则点可能只有 1 像素或不可见
f_Color = a_Color; // 片元无法直接读 attribute,须经此处 out 传递
}
`;
// 片元着色器:只能接收顶点 out 对应的 in,不能直接读 JS attribute
// vec4(R,G,B,A) 各分量 0.0~1.0;WebGL2 无 gl_FragColor,需声明 out 输出最终颜色
const fragmentShaderSource = `#version 300 es
precision mediump float;
in vec4 f_Color; // 与顶点 out f_Color 配对,由 GPU 在三角形内插值后传入
out vec4 fragColor;
void main() {
fragColor = f_Color;
}
`;
// 编译并链接着色器,得到 program(准备工作,此时还未绘制)
const program = initShaderProgram(gl, vertexShaderSource, fragmentShaderSource);
if (!program) return;
gl.useProgram(program);
glRef.current = gl;
programRef.current = program;
}
function renderRectangle(
x: number,
y: number,
width: number,
height: number,
color: [number, number, number, number],
) {
const gl = glRef.current;
if (!gl) return;
const program = programRef.current;
if (!program) return;
const aPosition = gl.getAttribLocation(program, 'a_Position');
const aColor = gl.getAttribLocation(program, 'a_Color');
const points: number[] = [];
points.push(
getX(x + width / 2, canvasRef.current.width),
getY(y - height / 2, canvasRef.current.height),
getX(x + width / 2, canvasRef.current.width),
getY(y + height / 2, canvasRef.current.height),
getX(x - width / 2, canvasRef.current.width),
getY(y - height / 2, canvasRef.current.height),
getX(x - width / 2, canvasRef.current.width),
getY(y + height / 2, canvasRef.current.height),
);
const buffer = gl.createBuffer();
// 绑定buffer
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(points), gl.STATIC_DRAW);
const stride = 2 * Float32Array.BYTES_PER_ELEMENT;
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, stride, 0);
gl.enableVertexAttribArray(aPosition);
// a_Color 二选一:enable 从 buffer 读(需 stride 含 rgba);disable 则用 vertexAttrib4f 常量
// buffer 仅有 x,y,须 disable,否则 vertexAttrib4f 被忽略,颜色/alpha 异常导致不可见
gl.disableVertexAttribArray(aColor);
gl.vertexAttrib4f(aColor, ...color);
// 绘制
gl.drawArrays(gl.TRIANGLE_STRIP, 0, points.length / 2);
}
function handleMouseDown(event: React.MouseEvent<HTMLCanvasElement>) {
// 坐标转换:offsetX/Y 相对 canvas 左上角(勿用 clientX/Y,canvas 不在页面原点时会偏移)
// 裁剪空间 x∈[-1,1];y 需翻转(屏幕 Y 向下,WebGL Y 向上)
const x = event.nativeEvent.offsetX;
const y = event.nativeEvent.offsetY;
rectangleRef.current.push({
points: [x, y],
width: randomWidthAndHeight(),
height: randomWidthAndHeight(),
color: [Math.random(), Math.random(), Math.random(), 1.0],
});
// 清屏:设背景色(黑)并清空颜色缓冲区
const gl = glRef.current;
if (!gl) return;
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
rectangleRef.current.forEach((rectangle) => {
renderRectangle(
rectangle.points[0],
rectangle.points[1],
rectangle.width,
rectangle.height,
rectangle.color,
);
});
}
// 组件挂载后初始化 WebGL2 并绘制
useEffect(() => {
initCanvas();
}, []);
return (
<div>
{/* width/height 为画布像素尺寸,非 CSS 尺寸 */}
<canvas ref={canvasRef} width="600" height="400" onMouseDown={handleMouseDown}></canvas>
</div>
);
};
export default WebGL;