目录
WebGL是一个强大的工具,可以用来在Web浏览器中创建复杂的3D图形。虽然它的设计初衷是为了3D渲染,但也可以用于创建2D内容。通过巧妙地利用几何、投影和纹理,我们可以构建出各种2D图形。
创建画布
首先,我们需要在HTML中设置一个<canvas>元素,用于承载WebGL渲染的内容:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGL 2D Example</title>
</head>
<body>
<canvas id="webgl-canvas" width="600" height="400"></canvas>
<script src="main.js"></script>
</body>
</html>
2D渲染
// 获取canvas元素和WebGL上下文
const canvas = document.getElementById('webgl-canvas');
const gl = canvas.getContext('webgl', { antialias: false });
// 设置视口大小和清除颜色
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(1, 1, 1, 1); // 白色背景
gl.clear(gl.COLOR_BUFFER_BIT);
// 定义2D坐标变换矩阵
const projectionMatrix = mat3.create();
mat3.identity(projectionMatrix);
mat3.translate(projectionMatrix, projectionMatrix, [-1, -1, 0]);
mat3.scale(projectionMatrix, projectionMatrix, [2 / canvas.width, 2 / canvas.height, 1]);
// 创建顶点着色器和片段着色器
const vertexShaderSource = `
attribute vec2 position;
uniform mat3 projectionMatrix;
void main() {
gl_Position = vec4((projectionMatrix * vec3(position, 1)).xy, 0, 1);
}
`;
const fragmentShaderSource = `
precision mediump float;
uniform vec4 color;
void main() {
gl_FragColor = color;
}
`;
// 编译和链接着色器
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
const program = createProgram(gl, vertexShader, fragmentShader);
// 获取顶点位置和颜色的属性位置
const positionAttributeLocation = gl.getAttribLocation(program, 'position');
const colorUniformLocation = gl.getUniformLocation(program, 'color');
// 创建顶点缓冲区并设置顶点数据
const vertices = [
-1, -1, // 左下角
1, -1, // 右下角
-1, 1, // 左上角
1, 1, // 右上角
];
const positionBuffer = createBuffer(gl, vertices);
// 绘制矩形
function drawRectangle(color) {
gl.useProgram(program);
gl.uniform4fv(colorUniformLocation, color);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.enableVertexAttribArray(positionAttributeLocation);
gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, vertices.length / 2);
}
// 初始化绘图
drawRectangle([0, 0, 255, 255]); // 蓝色矩形
// 动画循环
function animate() {
requestAnimationFrame(animate);
// 更新颜色
let t = performance.now() / 1000;
let r = Math.sin(t) * 128 + 128;
let g = Math.cos(t) * 128 + 128;
let b = 255;
drawRectangle([r / 255, g / 255, b / 255, 1]);
// 渲染到屏幕
gl.flush();
}
animate();
上述代码中,设置了一个2D投影矩阵,将[-1, 1]的坐标范围映射到canvas的[0, width]和[0, height]。接着,我们创建了顶点着色器和片段着色器,用于处理顶点位置和颜色。然后,我们设置了顶点数据并创建了一个缓冲区,用于存储矩形的四个顶点。最后,我们定义了一个drawRectangle函数,用于绘制矩形,并在动画循环中不断改变颜色。
通过对顶点进行2D坐标变换实现的。在顶点着色器中,我们使用了projectionMatrix来转换顶点位置,使其适应canvas的2D坐标系。在片段着色器中,我们简单地将颜色传入并输出。
修改顶点着色器
添加一个rotation
和scale uniforms
:
const vertexShaderSource = `
attribute vec2 position;
uniform mat3 projectionMatrix;
uniform float rotation;
uniform vec2 scale;
void main() {
// 应用旋转和平移
vec2 rotatedPosition = vec2(
position.x * cos(rotation) - position.y * sin(rotation),
position.x * sin(rotation) + position.y * cos(rotation)
);
// 应用缩放
vec2 scaledPosition = vec2(
rotatedPosition.x * scale.x,
rotatedPosition.y * scale.y
);
gl_Position = vec4((projectionMatrix * vec3(scaledPosition, 1)).xy, 0, 1);
}
`;
创建和更新这些uniforms:
// 新增旋转和缩放uniforms
const rotationUniformLocation = gl.getUniformLocation(program, 'rotation');
const scaleUniformLocation = gl.getUniformLocation(program, 'scale');
// 修改drawRectangle函数,添加旋转和缩放
function drawRectangle(color, rotation, scale) {
gl.useProgram(program);
gl.uniform4fv(colorUniformLocation, color);
gl.uniform1f(rotationUniformLocation, rotation);
gl.uniform2fv(scaleUniformLocation, scale);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.enableVertexAttribArray(positionAttributeLocation);
gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, vertices.length / 2);
}
// 动画循环
function animate() {
requestAnimationFrame(animate);
// 更新旋转和缩放
let t = performance.now() / 1000;
let rotationValue = t * Math.PI * 2; // 旋转角度
let scaleX = 1 + Math.sin(t) * 0.2; // 缩放比例X
let scaleY = 1 + Math.cos(t) * 0.2; // 缩放比例Y
// 绘制旋转和缩放的矩形
drawRectangle([0, 0, 255, 255], rotationValue, [scaleX, scaleY]);
// 渲染到屏幕
gl.flush();
}
animate();
现在,矩形会随着时间的推移而旋转和缩放。rotation uniform用于控制旋转角度,scale uniform是一个二维向量,用于控制X轴和Y轴的缩放比例。
平移
修改drawRectangle函数以接受平移参数,并在动画循环中更新平移值:
// 新增平移uniform
const translationUniformLocation = gl.getUniformLocation(program, 'translation');
// 修改drawRectangle函数,添加平移
function drawRectangle(color, rotation, scale, translation) {
gl.useProgram(program);
gl.uniform4fv(colorUniformLocation, color);
gl.uniform1f(rotationUniformLocation, rotation);
gl.uniform2fv(scaleUniformLocation, scale);
gl.uniform2fv(translationUniformLocation, translation);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.enableVertexAttribArray(positionAttributeLocation);
gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, vertices.length / 2);
}
// 动画循环
function animate() {
requestAnimationFrame(animate);
// 更新旋转、缩放和平移
let t = performance.now() / 1000;
let rotationValue = t * Math.PI * 2;
let scaleX = 1 + Math.sin(t) * 0.2;
let scaleY = 1 + Math.cos(t) * 0.2;
let translationX = Math.sin(t) * canvas.width * 0.2;
let translationY = Math.cos(t) * canvas.height * 0.2;
// 绘制旋转、缩放和平移的矩形
drawRectangle([0, 0, 255, 255], rotationValue, [scaleX, scaleY], [translationX, translationY]);
// 渲染到屏幕
gl.flush();
}
animate();
上面添加了一个新的uniform translation
,用于控制矩形的平移。在动画循环中,我们计算平移值,使其随时间变化。这样,矩形不仅会旋转和缩放,还会在canvas内移动。
WebGL还支持纹理贴图,可以用于创建复杂的2D图像。假设我们有一个名为texture.png的图像,我们可以将其加载并应用于矩形:
// 加载纹理
const texture = loadTexture(gl, 'texture.png');
// 在drawRectangle中应用纹理
function drawRectangle(color, rotation, scale, translation, texture) {
// ...
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.uniform1i(gl.getUniformLocation(program, 'texture'), 0);
// ...
}
// 创建纹理
function loadTexture(gl, url) {
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
const level = 0;
const internalFormat = gl.RGBA;
const border = 0;
const srcFormat = gl.RGBA;
const srcType = gl.UNSIGNED_BYTE;
const pixelData = null;
gl.texImage2D(
gl.TEXTURE_2D,
level,
internalFormat,
border,
srcFormat,
srcType,
pixelData
);
const image = new Image();
image.onload = () => {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.generateMipmap(gl.TEXTURE_2D);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
};
image.src = url;
return texture;
}
// 修改drawRectangle调用,传递纹理
drawRectangle([1, 1, 1, 1], rotationValue, [scaleX, scaleY], [translationX, translationY], texture);
现在,随着旋转、缩放和平移,图像也会相应地改变。
光照
在WebGL中,光照通常在片段着色器中计算。你需要定义光源的位置、颜色以及物体表面的属性(如法线)。
const fragmentShaderSource = `
precision mediump float;
uniform vec4 color;
uniform vec3 lightPosition;
varying vec3 vNormal;
varying vec3 vWorldPosition;
void main() {
vec3 lightDirection = normalize(lightPosition - vWorldPosition);
vec3 diffuse = max(dot(vNormal, lightDirection), 0.0) * color.rgb;
gl_FragColor = vec4(diffuse, color.a);
}
`;
// 在顶点着色器中传递法线和世界位置
const vertexShaderSource = `
attribute vec3 position;
attribute vec3 normal;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
varying vec3 vNormal;
varying vec3 vWorldPosition;
void main() {
vNormal = normalize(mat3(modelViewMatrix) * normal);
vWorldPosition = vec3(modelViewMatrix * vec4(position, 1.0));
gl_Position = projectionMatrix * vec4(vWorldPosition, 1.0);
}
`;
在上述代码中,我们计算了光线与物体表面的点的法线向量的点积,用于确定漫反射光的强度。请注意,实际的光照模型可能更复杂,包括镜面高光、环境光等。
深度测试
WebGL默认启用深度测试,但你可以通过gl.enable(gl.DEPTH_TEST)
来确认。深度测试用于确定哪些像素应该在前景,哪些应该在背景。确保在绘制3D形状时,先绘制远处的形状,再绘制近处的形状。
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LESS); // 使用小于比较函数
混合模式
在WebGL中,混合模式通过gl.blendFunc
和gl.blendEquation
设置。
gl.enable(gl.BLEND);
gl.blendEquation(gl.FUNC_ADD);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);